diff --git a/changes/1220.feature.md b/changes/1220.feature.md new file mode 100644 index 0000000000..c1c62259b6 --- /dev/null +++ b/changes/1220.feature.md @@ -0,0 +1 @@ +Add support for the auto-moderation API. diff --git a/hikari/__init__.py b/hikari/__init__.py index 45a0066f97..d767ae5802 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -69,6 +69,7 @@ from hikari.applications import TeamMembershipState from hikari.applications import TokenType from hikari.audit_logs import * +from hikari.auto_mod import * from hikari.channels import * from hikari.colors import * from hikari.colours import * diff --git a/hikari/__init__.pyi b/hikari/__init__.pyi index 40b01ca6cc..2afeffa10d 100644 --- a/hikari/__init__.pyi +++ b/hikari/__init__.pyi @@ -47,6 +47,7 @@ from hikari.applications import TeamMember as TeamMember from hikari.applications import TeamMembershipState as TeamMembershipState from hikari.applications import TokenType as TokenType from hikari.audit_logs import * +from hikari.auto_mod import * from hikari.channels import * from hikari.colors import * from hikari.colours import * diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index f2a8e63647..f6a0284671 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -33,6 +33,7 @@ if typing.TYPE_CHECKING: from hikari import applications as application_models from hikari import audit_logs as audit_log_models + from hikari import auto_mod as auto_mod_models from hikari import channels as channel_models from hikari import commands from hikari import embeds as embed_models @@ -1997,3 +1998,52 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhook_model hikari.errors.UnrecognisedEntityError If the channel type is unknown. """ + + ################### + # AUTO-MOD MODELS # + ################### + + @abc.abstractmethod + def deserialize_auto_mod_action(self, payload: data_binding.JSONObject) -> auto_mod_models.PartialAutoModAction: + """Parse a raw payload from Discord into an auto-moderation action object. + + Parameters + ---------- + payload : hikari.internal.data_binding.JSONObject + The JSON payload to deserialize. + + Returns + ------- + hikari.auto_mod.PartialAutoModAction + The deserialized auto-moderation action object. + """ + + @abc.abstractmethod + def serialize_auto_mod_action(self, action: auto_mod_models.PartialAutoModAction) -> data_binding.JSONObject: + """Serialize an auto-moderation action object to a json serializable dict. + + Parameters + ---------- + overwrite : hikari.auto_mod.PartialAutoModAction + The auto-moderation action object to serialize. + + Returns + ------- + hikari.internal.data_binding.JSONObject + The serialized representation of the auto-moderation action object. + """ + + @abc.abstractmethod + def deserialize_auto_mod_rule(self, payload: data_binding.JSONObject) -> auto_mod_models.AutoModRule: + """Parse a raw payload from Discord into an auto-moderation rule object. + + Parameters + ---------- + payload : hikari.internal.data_binding.JSONObject + The JSON payload to deserialize. + + Returns + ------- + hikari.auto_mod.AutoModRule + The deserialized auto-moderation rule object. + """ diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py index 438f96e6e6..ee66ec3627 100644 --- a/hikari/api/event_factory.py +++ b/hikari/api/event_factory.py @@ -42,6 +42,7 @@ from hikari import voices as voices_models from hikari.api import shard as gateway_shard from hikari.events import application_events + from hikari.events import auto_mod_events from hikari.events import channel_events from hikari.events import guild_events from hikari.events import interaction_events @@ -1410,3 +1411,79 @@ def deserialize_voice_server_update_event( hikari.events.voice_events.VoiceServerUpdateEvent The parsed voice server update event object. """ + + @abc.abstractmethod + def deserialize_auto_mod_rule_create_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> auto_mod_events.AutoModRuleCreateEvent: + """Parse a raw payload from Discord into an auto-mod rule create event object. + + Parameters + ---------- + shard : hikari.api.shard.GatewayShard + The shard that emitted this event. + payload : hikari.internal.data_binding.JSONObject + The dict payload to parse. + + Returns + ------- + hikari.events.voice_events.AutoModRuleCreateEvent + The parsed auto-mod rule create event object. + """ + + @abc.abstractmethod + def deserialize_auto_mod_rule_update_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> auto_mod_events.AutoModRuleUpdateEvent: + """Parse a raw payload from Discord into an auto-mod rule update event object. + + Parameters + ---------- + shard : hikari.api.shard.GatewayShard + The shard that emitted this event. + payload : hikari.internal.data_binding.JSONObject + The dict payload to parse. + + Returns + ------- + hikari.events.voice_events.AutoModRuleUpdateEvent + The parsed auto-mod rule update event object. + """ + + @abc.abstractmethod + def deserialize_auto_mod_rule_delete_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> auto_mod_events.AutoModRuleDeleteEvent: + """Parse a raw payload from Discord into an auto-mod rule delete event object. + + Parameters + ---------- + shard : hikari.api.shard.GatewayShard + The shard that emitted this event. + payload : hikari.internal.data_binding.JSONObject + The dict payload to parse. + + Returns + ------- + hikari.events.voice_events.AutoModRuleDeleteEvent + The parsed auto-mod rule delete event object. + """ + + @abc.abstractmethod + def deserialize_auto_mod_action_execution_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> auto_mod_events.AutoModActionExecutionEvent: + """Parse a raw payload from Discord into an auto-mod action execution event object. + + Parameters + ---------- + shard : hikari.api.shard.GatewayShard + The shard that emitted this event. + payload : hikari.internal.data_binding.JSONObject + The dict payload to parse. + + Returns + ------- + hikari.events.voice_events.AutoModActionExecutionEvent + The parsed auto-mod action execution event object. + """ diff --git a/hikari/api/rest.py b/hikari/api/rest.py index e638d8f649..fb4d45d422 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -36,6 +36,7 @@ if typing.TYPE_CHECKING: from hikari import applications from hikari import audit_logs + from hikari import auto_mod from hikari import channels as channels_ from hikari import colors from hikari import commands @@ -8218,3 +8219,292 @@ def fetch_scheduled_event_users( hikari.errors.InternalServerError If an internal error occurs on Discord while handling the request. """ + + @abc.abstractmethod + async def fetch_auto_mod_rules( + self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], / + ) -> typing.Sequence[auto_mod.AutoModRule]: + """Fetch a guild's auto-moderation rules. + + Parameters + ---------- + guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild] + Object or ID of the guild to fetch the auto-moderation rules of. + + Returns + ------- + typing.Sequence[hikari.auto_mod.AutoModRule] + Sequence of the guild's auto-moderation rules. + + Raises + ------ + hikari.errors.BadRequestError + If any of the fields that are passed have an invalid value. + hikari.errors.ForbiddenError + If you are missing the `MANAGE_GUILD` permission. + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the guild was not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def fetch_auto_mod_rule( + self, + guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], + rule: snowflakes.SnowflakeishOr[auto_mod.AutoModRule], + /, + ) -> auto_mod.AutoModRule: + """Fetch a auto-moderation rule. + + Parameters + ---------- + guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild] + Object or ID of the guild to fetch the auto-moderation rules of. + rule : hikari.snowflakes.SnowflakeishOr[hikari.auto_mod.AutoModRule] + Object or ID of the auto-moderation rule to fetch. + + Returns + ------- + hikari.auto_mod.AutoModRule + The fetched auto-moderation rule. + + Raises + ------ + hikari.errors.BadRequestError + If any of the fields that are passed have an invalid value. + hikari.errors.ForbiddenError + If you are missing the `MANAGE_GUILD` permission. + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the guild or rule was not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def create_auto_mod_rule( + self, + guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], + /, + name: str, + event_type: typing.Union[int, auto_mod.AutoModEventType], + trigger_type: typing.Union[int, auto_mod.AutoModTriggerType], + actions: typing.Sequence[auto_mod.PartialAutoModAction], + allow_list: undefined.UndefinedOr[typing.Sequence[str]] = undefined.UNDEFINED, + keyword_filter: undefined.UndefinedOr[typing.Sequence[str]] = undefined.UNDEFINED, + presets: undefined.UndefinedOr[ + typing.Sequence[typing.Union[int, auto_mod.AutoModKeywordPresetType]] + ] = undefined.UNDEFINED, + enabled: undefined.UndefinedOr[bool] = True, + exempt_channels: undefined.UndefinedOr[ + snowflakes.SnowflakeishSequence[channels_.PartialChannel] + ] = undefined.UNDEFINED, + exempt_roles: undefined.UndefinedOr[snowflakes.SnowflakeishSequence[guilds.PartialRole]] = undefined.UNDEFINED, + reason: undefined.UndefinedOr[str] = undefined.UNDEFINED, + ) -> auto_mod.AutoModRule: + """Create an auto-moderation rule. + + Parameters + ---------- + guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild] + Object or ID of the guild to create the auto-moderation rules in. + name : builtins.str + The rule's name. + event_type : typing.Union[builtins.int, hikari.auto_mod.AutoModEventType] + The type of user content creation event this rule should trigger on. + trigger_type : typing.Union[builtins.int, hikari.auto_mod.AutoModTriggerType] + The type of user content creation this should trigger on. + + Note that the required fields very dependent on what this is set to. + actions : typing.Sequence[hikari.auto_mod.PartialAutoModAction] + Sequence of the actions to execute when this rule is triggered. + allow_list : hikari.undefined.UndefinedOr[typing.Sequence[builtins.str]] + A sequence of filters which will be exempt from triggering the preset trigger. + + This supports a wildcard matching strategy which is documented at + https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-keyword-matching-strategies. + + This can only be set for KEYWORD_PRESET triggers. + keyword_filter : hikari.undefined.UndefinedOr[typing.Sequence[builtins.str]] + A sequence of filter strings this trigger checks for. + + This supports a wildcard matching strategy which is documented at + https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-keyword-matching-strategies. + + This is required for and can only be set for KEYWORD triggers. + presets : hikari.undefined.UndefinedOr[typing.Sequence[typing.Union[builtins.int, hikari.auto_mod.AutoModKeywordPresetType]]] + Sequence of Discord's presets to match for. + + This is required for and can only be set for KEYWORD_PRESET triggers. + enabled : hikari.undefined.UndefinedOr[builtins.bool] + Whether this auto-moderation rule should be enabled. + exempt_channels : hikari.undefined.UndefinedOr[hikari.snowflakes.SnowflakeishSequence[hikari.channels.PartialChannel]] + Sequence of up to 50 objects and IDs of channels which are not + effected by the rule. + exempt_roles : hikari.undefined.UndefinedOr[hikari.snowflakes.SnowflakeishSequence[hikari.guilds.PartialRole]] + Sequence of up to 20 objects and IDs of roles which are not + effected by the rule. + reason : hikari.undefined.UndefinedOr[builtins.str] + If provided, the reason that will be recorded in the audit logs. + Maximum of 512 characters. + + Returns + ------- + hikari.auto_mod.AutoModRule + The created auto-moderation rule. + + Raises + ------ + hikari.errors.BadRequestError + If any of the fields that are passed have an invalid value. + hikari.errors.ForbiddenError + If you are missing the `MANAGE_GUILD` permission or if you try to + set a TIMEOUT action without the `MODERATE_MEMBERS` permission. + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the guild was not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + async def edit_auto_mod_rule( + self, + guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], + rule: snowflakes.SnowflakeishOr[auto_mod.AutoModRule], + /, + name: undefined.UndefinedOr[str] = undefined.UNDEFINED, + event_type: undefined.UndefinedOr[typing.Union[int, auto_mod.AutoModEventType]] = undefined.UNDEFINED, + actions: undefined.UndefinedOr[typing.Sequence[auto_mod.PartialAutoModAction]] = undefined.UNDEFINED, + allow_list: undefined.UndefinedOr[typing.Sequence[str]] = undefined.UNDEFINED, + keyword_filter: undefined.UndefinedOr[typing.Sequence[str]] = undefined.UNDEFINED, + presets: undefined.UndefinedOr[ + typing.Sequence[typing.Union[int, auto_mod.AutoModKeywordPresetType]] + ] = undefined.UNDEFINED, + enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + exempt_channels: undefined.UndefinedOr[ + snowflakes.SnowflakeishSequence[channels_.PartialChannel] + ] = undefined.UNDEFINED, + exempt_roles: undefined.UndefinedOr[snowflakes.SnowflakeishSequence[guilds.PartialRole]] = undefined.UNDEFINED, + reason: undefined.UndefinedOr[str] = undefined.UNDEFINED, + ) -> auto_mod.AutoModRule: + """Edit an auto-moderation rule. + + Parameters + ---------- + guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild] + Object or ID of the guild to edit an auto-moderation rule in. + rule : hikari.snowflakes.SnowflakeishOr[hikari.auto_mod.AutoModRule] + Object or ID of the auto-moderation rule to edit. + name : hikari.undefined.UndefinedOr[builtins.str] + If specified, the rule's new name. + event_type : hikari.undefined.UndefinedOr[typing.Union[builtins.int, hikari.auto_mod.AutoModEventType]] + If specified, the type of user content creation event this rule + should trigger on. + actions : hikari.undefined.UndefinedOr[typing.Sequence[hikari.auto_mod.PartialAutoModAction]] + If specified, a sequence of the actions to execute when this rule + is triggered. + allow_list : hikari.undefined.UndefinedOr[typing.Sequence[builtins.str]] + If specified, a sequence of filters which will be exempt from + triggering the preset trigger. + + This supports a wildcard matching strategy which is documented at + https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-keyword-matching-strategies. + + This can only be set for KEYWORD_PRESET triggers. + keyword_filter : hikari.undefined.UndefinedOr[typing.Sequence[builtins.str]] + If specified, a sequence of filter strings this trigger checks for. + + This supports a wildcard matching strategy which is documented at + https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-keyword-matching-strategies. + + This is required for and can only be set for KEYWORD triggers. + presets : hikari.undefined.UndefinedOr[typing.Sequence[typing.Union[builtins.int, hikari.auto_mod.AutoModKeywordPresetType]]] + If specified, a sequence of Discord's presets to match for. + + This is required for and can only be set for KEYWORD_PRESET triggers. + enabled : hikari.undefined.UndefinedOr[builtins.bool] + If specified, whether this auto-moderation rule should be enabled. + exempt_channels : hikari.undefined.UndefinedOr[hikari.snowflakes.SnowflakeishSequence[hikari.channels.PartialChannel]] + If specified, a sequence of up to 50 objects and IDs of channels + which are not effected by the rule. + exempt_roles : hikari.undefined.UndefinedOr[hikari.snowflakes.SnowflakeishSequence[hikari.guilds.PartialRole]] + If specified, a sequence of up to 20 objects and IDs of roles which + are not effected by the rule. + reason : hikari.undefined.UndefinedOr[builtins.str] + If provided, the reason that will be recorded in the audit logs. + Maximum of 512 characters. + + Returns + ------- + hikari.auto_mod.AutoModRule + The created auto-moderation rule. + + Raises + ------ + hikari.errors.BadRequestError + If any of the fields that are passed have an invalid value. + hikari.errors.ForbiddenError + If you are missing the `MANAGE_GUILD` permission or if you try to + set a TIMEOUT action without the `MODERATE_MEMBERS` permission. + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the guild was not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + async def delete_auto_mod_rule( + self, + guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], + rule: snowflakes.SnowflakeishOr[auto_mod.AutoModRule], + /, + reason: undefined.UndefinedOr[str] = undefined.UNDEFINED, + ) -> None: + """Create an auto-moderation rule. + + Parameters + ---------- + guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild] + Object or ID of the guild to delete the auto-moderation rules of. + rule : hikari.snowflakes.SnowflakeishOr[hikari.auto_mod.AutoModRule] + Object or ID of the auto-moderation rule to delete. + reason : hikari.undefined.UndefinedOr[builtins.str] + If provided, the reason that will be recorded in the audit logs. + Maximum of 512 characters. + + Raises + ------ + hikari.errors.BadRequestError + If any of the fields that are passed have an invalid value. + hikari.errors.ForbiddenError + If you are missing the `MANAGE_GUILD` permission. + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the guild or rule was not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ diff --git a/hikari/applications.py b/hikari/applications.py index d7a3f17197..1a3e1be7ae 100644 --- a/hikari/applications.py +++ b/hikari/applications.py @@ -300,7 +300,7 @@ class OwnGuild(guilds.PartialGuild): """Represents a user bound partial guild object.""" features: typing.Sequence[typing.Union[str, guilds.GuildFeature]] = attr.field(eq=False, hash=False, repr=False) - """A list of the features in this guild.""" + """A sequence of the features in this guild.""" is_owner: bool = attr.field(eq=False, hash=False, repr=True) """`True` when the current user owns this guild.""" diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index fddfaf3422..fe322fe900 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -54,6 +54,7 @@ if typing.TYPE_CHECKING: import datetime + from hikari import auto_mod from hikari import guilds from hikari import messages from hikari import traits @@ -334,6 +335,10 @@ class AuditLogEventType(int, enums.Enum): GUILD_SCHEDULED_EVENT_UPDATE = 101 GUILD_SCHEDULED_EVENT_DELETE = 102 APPLICATION_COMMAND_PERMISSION_UPDATE = 121 + AUTO_MODERATION_RULE_CREATE = 140 + AUTO_MODERATION_RULE_UPDATE = 141 + AUTO_MODERATION_RULE_DELETE = 142 + AUTO_MODERATION_BLOCK_MESSAGE = 143 @attr.define(hash=False, kw_only=True, weakref_slot=False) @@ -592,6 +597,9 @@ async def fetch_user(self) -> typing.Optional[users_.User]: class AuditLog(typing.Sequence[AuditLogEntry]): """Represents a guilds audit log's page.""" + auto_mod_rules: typing.Mapping[snowflakes.Snowflake, auto_mod.AutoModRule] = attr.field(repr=False) + """A mapping of the auto-moderation rule objects referenced in this audit log.""" + entries: typing.Mapping[snowflakes.Snowflake, AuditLogEntry] = attr.field(repr=False) """A mapping of snowflake IDs to the audit log's entries.""" diff --git a/hikari/auto_mod.py b/hikari/auto_mod.py new file mode 100644 index 0000000000..07e0c8d5fe --- /dev/null +++ b/hikari/auto_mod.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa +# +# 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. +"""Entities that are used to describe auto-moderation on Discord.""" +from __future__ import annotations + +__all__: typing.Sequence[str] = ( + "AutoModActionType", + "PartialAutoModAction", + "AutoModBlockMessage", + "AutoModSendAlertMessage", + "AutoModTimeout", + "AutoModEventType", + "AutoModTriggerType", + "AutoModKeywordPresetType", + "PartialAutoModTrigger", + "KeywordTrigger", + "HarmfulLinkTrigger", + "SpamTrigger", + "KeywordPresetTrigger", + "AutoModRule", +) + +import typing + +import attr + +from hikari import snowflakes +from hikari.internal import attr_extensions +from hikari.internal import enums + +if typing.TYPE_CHECKING: + import datetime + + from hikari import traits + + +class AutoModActionType(int, enums.Enum): + """The type of an auto-moderation rule action.""" + + BLOCK_MESSAGES = 1 + """Block the content of the triggering message.""" + + SEND_ALERT_MESSAGE = 2 + """Log the triggering user content to a specified channel.""" + + TIMEOUT = 3 + """Timeout the triggering message's author for a specified duration. + + This type can only be set for `KEYWORD` rules and requires the `MODERATE_MEMBERS` + permission to use. + """ + + +@attr_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +class PartialAutoModAction: + """Base class for an action which is executed when a rule is triggered.""" + + type: AutoModActionType = attr.field() + """The type of auto-moderation action.""" + + +@attr.define(kw_only=True, weakref_slot=False) +class AutoModBlockMessage(PartialAutoModAction): + """Block the content of the triggering message.""" + + +@attr.define(kw_only=True, weakref_slot=False) +class AutoModSendAlertMessage(PartialAutoModAction): + """Log the triggering content to a specific channel.""" + + channel_id: snowflakes.Snowflake = attr.field() + """ID of the channel to log trigger events to.""" + + +@attr.define(kw_only=True, weakref_slot=False) +class AutoModTimeout(PartialAutoModAction): + """Timeout the triggering message's author for a specified duration. + + This type can only be set for `KEYWORD` rules and requires the `MODERATE_MEMBERS` + permission to use. + """ + + duration: datetime.timedelta = attr.field() + """The total seconds to timeout the user for (max 2419200 seconds/4 weeks).""" + + +class AutoModEventType(int, enums.Enum): + """Type of event to check for an auto-moderation rule.""" + + MESSAGE_SEND = 1 + """When a member sends or edits a message in the guild.""" + + +class AutoModTriggerType(int, enums.Enum): + """Type of trigger for an auto-moderation rule.""" + + KEYWORD = 1 + """Match message content against a list of keywords.""" + + HARMFUL_LINK = 2 + """Scan messages for links which are deemed "harmful" by Discord.""" + + SPAM = 3 + """Discord's guild anti-spam system.""" + + KEYWORD_PRESET = 4 + """Discord's preset keyword triggers.""" + + +class AutoModKeywordPresetType(int, enums.Enum): + """Discord's KEYWORD_PRESET type.""" + + PROFANITY = 1 + """Trigger on words which may be considered forms of swearing or cursing.""" + + SEXUAL_CONTENT = 2 + """Trigger on words that refer to explicit behaviour or activity.""" + + SLURS = 3 + """Trigger on personal insults and words which "may be considered hate speech".""" + + +@attr_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +class PartialAutoModTrigger: + """Base class representing the content a rule triggers on.""" + + type: AutoModTriggerType = attr.field(eq=False, hash=False, repr=False) + """The type action this triggers.""" + + +@attr.define(kw_only=True, weakref_slot=False) +class KeywordTrigger(PartialAutoModTrigger): + """A trigger based on matching message content against a list of keywords.""" + + keyword_filter: typing.Sequence[str] = attr.field(eq=False, hash=False, repr=False) + """The filter strings this trigger checks for. + + This supports a wildcard matching strategy which is documented at + https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-keyword-matching-strategies. + """ + + +class HarmfulLinkTrigger(PartialAutoModTrigger): + """A trigger based on Discord's own list of links deemed "harmful".""" + + __slots__: typing.Sequence[str] = [] + + +class SpamTrigger(PartialAutoModTrigger): + """A trigger based on Discord's spam detection.""" + + __slots__: typing.Sequence[str] = [] + + +@attr.define(kw_only=True, weakref_slot=False) +class KeywordPresetTrigger(PartialAutoModTrigger): + """A trigger based on a predefined set of presets provided by Discord.""" + + allow_list: typing.Sequence[str] = attr.field(eq=False, factory=list, hash=False, repr=False) + """A sequence of filters which will be exempt from triggering the preset trigger. + + This supports a wildcard matching strategy which is documented at + https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-keyword-matching-strategies. + """ + + presets: typing.Sequence[typing.Union[int, AutoModKeywordPresetType]] = attr.field(eq=False, hash=False, repr=False) + """The predefined presets provided by Discord to match against.""" + + +@attr_extensions.with_copy +@attr.define(hash=True, kw_only=True, weakref_slot=False) +class AutoModRule(snowflakes.Unique): + """Auto moderation rule which defines how user content is filtered.""" + + app: traits.RESTAware = attr.field( + repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True} + ) + """The client application that models may use for procedures.""" + + id: snowflakes.Snowflake = attr.field(eq=True, hash=True, repr=True) + """The ID of this entity.""" + + guild_id: snowflakes.Snowflake = attr.field(eq=False, hash=False, repr=True) + """ID of the guild this rule belongs to.""" + + name: str = attr.field(eq=False, hash=False, repr=True) + """The rule's name.""" + + creator_id: snowflakes.Snowflake = attr.field(eq=False, hash=False, repr=True) + """ID of the user who originally created this rule.""" + + event_type: AutoModEventType = attr.field(eq=False, hash=False, repr=True) + """The type of event this rule triggers on.""" + + trigger: PartialAutoModTrigger = attr.field(eq=False, hash=False, repr=False) + """The content this rule triggers on.""" + + actions: typing.Sequence[PartialAutoModAction] = attr.field(eq=False, hash=False, repr=False) + """Sequence of the actions which will execute when this rule is triggered.""" + + is_enabled: bool = attr.field(eq=False, hash=False, repr=False) + """Whether this rule is enabled.""" + + exempt_channel_ids: typing.Sequence[snowflakes.Snowflake] = attr.field(eq=False, hash=False, repr=False) + """A sequence of IDs of (up to 20) channels which aren't effected by this rule.""" + + exempt_role_ids: typing.Sequence[snowflakes.Snowflake] = attr.field(eq=False, hash=False, repr=False) + """A sequence of IDs of (up to 50) roles which aren't effected by this rule.""" diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index 2e858ab8e5..6227a31b30 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -25,6 +25,7 @@ from __future__ import annotations from hikari.events.application_events import * +from hikari.events.auto_mod_events import * from hikari.events.base_events import Event from hikari.events.base_events import ExceptionEvent from hikari.events.channel_events import * diff --git a/hikari/events/__init__.pyi b/hikari/events/__init__.pyi index 2bd26128da..9c0b70ea3f 100644 --- a/hikari/events/__init__.pyi +++ b/hikari/events/__init__.pyi @@ -2,6 +2,7 @@ # This file was automatically generated by `nox -s generate-stubs` from hikari.events.application_events import * +from hikari.events.auto_mod_events import * from hikari.events.base_events import Event as Event from hikari.events.base_events import ExceptionEvent as ExceptionEvent from hikari.events.channel_events import * diff --git a/hikari/events/auto_mod_events.py b/hikari/events/auto_mod_events.py new file mode 100644 index 0000000000..4922635c85 --- /dev/null +++ b/hikari/events/auto_mod_events.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa +# +# 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. +"""Events that fire for auto-moderation related changes.""" +from __future__ import annotations + +__all__: typing.Sequence[str] = ( + "AutoModEvent", + "AutoModRuleCreateEvent", + "AutoModRuleUpdateEvent", + "AutoModRuleDeleteEvent", + "AutoModActionExecutionEvent", +) + +import abc +import typing + +import attr + +from hikari import intents +from hikari.events import base_events +from hikari.events import shard_events +from hikari.internal import attr_extensions + +if typing.TYPE_CHECKING: + from hikari import auto_mod + from hikari import snowflakes + from hikari import traits + from hikari.api import shard as gateway_shard + + +@base_events.requires_intents(intents.Intents.AUTO_MODERATION_CONFIGURATION, intents.Intents.AUTO_MODERATION_EXECUTION) +class AutoModEvent(shard_events.ShardEvent, abc.ABC): + """Base class for auto-moderation gateway events.""" + + __slots__: typing.Sequence[str] = () + + +@base_events.requires_intents(intents.Intents.AUTO_MODERATION_CONFIGURATION) +@attr_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +class AutoModRuleCreateEvent(AutoModEvent): + """Event that's fired when an auto-moderation rule is created.""" + + shard: gateway_shard.GatewayShard = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <>. + + rule: auto_mod.AutoModRule = attr.field() + """Object of the auto-moderation rule which was created.""" + + @property + def app(self) -> traits.RESTAware: + # <>. + return self.rule.app + + +@base_events.requires_intents(intents.Intents.AUTO_MODERATION_CONFIGURATION) +@attr_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +class AutoModRuleUpdateEvent(AutoModEvent): + """Event that's fired when an auto-moderation rule is updated.""" + + shard: gateway_shard.GatewayShard = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <>. + + rule: auto_mod.AutoModRule = attr.field() + """Object of the auto-moderation rule which was updated.""" + + @property + def app(self) -> traits.RESTAware: + # <>. + return self.rule.app + + +@base_events.requires_intents(intents.Intents.AUTO_MODERATION_CONFIGURATION) +@attr_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +class AutoModRuleDeleteEvent(AutoModEvent): + """Event that's fired when an auto-moderation rule is deleted.""" + + shard: gateway_shard.GatewayShard = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <>. + + rule: auto_mod.AutoModRule = attr.field() + """Object of the auto-moderation rule which was deleted.""" + + @property + def app(self) -> traits.RESTAware: + # <>. + return self.rule.app + + +@base_events.requires_intents(intents.Intents.AUTO_MODERATION_EXECUTION) +@attr_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +class AutoModActionExecutionEvent(AutoModEvent): + """Event that's fired when an auto-mod action is executed.""" + + app: traits.RESTAware = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <>. + + shard: gateway_shard.GatewayShard = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <>. + + guild_id: snowflakes.Snowflake = attr.field(repr=True) + """ID of the guild this action was executed in.""" + + action: auto_mod.PartialAutoModAction = attr.field(repr=False) + """Object of the action which was executed.""" + + rule_id: snowflakes.Snowflake = attr.field() + """ID of the rule which was triggered.""" + + rule_trigger_type: typing.Union[int, auto_mod.AutoModTriggerType] = attr.field(repr=False) + """Type of the rule which was triggered.""" + + user_id: snowflakes.Snowflake = attr.field(repr=False) + """ID of the user who generated the context which triggered this.""" + + channel_id: typing.Optional[snowflakes.Snowflake] = attr.field(repr=False) + """ID of the channel the matching context was sent to. + + This will be `builtins.None` if the message was blocked by auto-moderation + of the matched content wasn't in a channel. + """ + + message_id: typing.Optional[snowflakes.Snowflake] = attr.field(repr=False) + """ID of the message the matching context was sent in. + + This will be `builtins.None` if the message was blocked by auto-moderation + or the matched content wasn't in a message. + """ + + alert_system_message_id: typing.Optional[snowflakes.Snowflake] = attr.field(repr=False) + """ID of any system auto-moderation messages posted as a result of this action. + + This will only be provided for `SEND_ALERT_MESSAGE` actions. + """ + + content: typing.Optional[str] = attr.field(repr=False) + """The user generated content which matched this rule. + + This will only be provided if the `MESSAGE_CONTENT` intent has + been declared. + """ + + matched_keyword: typing.Optional[str] = attr.field(repr=False) + """The word or phrase configured in the rule which was triggered, if it's a keyword trigger.""" + + matched_content: typing.Optional[str] = attr.field(repr=False) + """The substring in content which triggered the rule. + + This will only be provided if the `MESSAGE_CONTENT` intent has + been declared and this is a keyword or keyword preset trigger. + """ diff --git a/hikari/guilds.py b/hikari/guilds.py index 34893db957..9117fa08e6 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -2428,7 +2428,7 @@ class GuildPreview(PartialGuild): """A preview of a guild with the `GuildFeature.DISCOVERABLE` feature.""" features: typing.Sequence[typing.Union[str, GuildFeature]] = attr.field(eq=False, hash=False, repr=False) - """A list of the features in this guild.""" + """A sequence of the features in this guild.""" splash_hash: typing.Optional[str] = attr.field(eq=False, hash=False, repr=False) """The hash of the splash for the guild, if there is one.""" @@ -2532,7 +2532,7 @@ class Guild(PartialGuild): """A representation of a guild on Discord.""" features: typing.Sequence[typing.Union[str, GuildFeature]] = attr.field(eq=False, hash=False, repr=False) - """A list of the features in this guild.""" + """A sequence of the features in this guild.""" application_id: typing.Optional[snowflakes.Snowflake] = attr.field(eq=False, hash=False, repr=False) """The ID of the application that created this guild. diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 199252e3cc..8be13fb430 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -34,6 +34,7 @@ from hikari import applications as application_models from hikari import audit_logs as audit_log_models +from hikari import auto_mod as auto_mod_models from hikari import channels as channel_models from hikari import colors as color_models from hikari import commands @@ -430,6 +431,8 @@ class EntityFactoryImpl(entity_factory.EntityFactory): "_app", "_audit_log_entry_converters", "_audit_log_event_mapping", + "_auto_mod_action_mapping", + "_auto_mod_trigger_mapping", "_command_mapping", "_message_component_type_mapping", "_modal_component_type_mapping", @@ -498,6 +501,17 @@ def __init__(self, app: traits.RESTAware) -> None: audit_log_models.AuditLogEventType.MEMBER_DISCONNECT: self._deserialize_member_disconnect_entry_info, audit_log_models.AuditLogEventType.MEMBER_MOVE: self._deserialize_member_move_entry_info, } + self._auto_mod_action_mapping = { + auto_mod_models.AutoModActionType.BLOCK_MESSAGES: self._deserialize_auto_mod_block_message, + auto_mod_models.AutoModActionType.SEND_ALERT_MESSAGE: self._deserialize_auto_mod_block_send_alert_message, + auto_mod_models.AutoModActionType.TIMEOUT: self._deserialize_auto_mod_timeout, + } + self._auto_mod_trigger_mapping = { + auto_mod_models.AutoModTriggerType.HARMFUL_LINK: self._deserialize_auto_mod_harmful_link_trigger, + auto_mod_models.AutoModTriggerType.KEYWORD: self._deserialize_auto_mod_keyword_trigger, + auto_mod_models.AutoModTriggerType.KEYWORD_PRESET: self._deserialize_auto_mod_keyword_preset_trigger, + auto_mod_models.AutoModTriggerType.SPAM: self._deserialize_auto_mod_spam_trigger, + } self._command_mapping = { commands.CommandType.SLASH: self.deserialize_slash_command, commands.CommandType.USER: self.deserialize_context_menu_command, @@ -895,6 +909,16 @@ def deserialize_audit_log( else: entries[entry.id] = entry + auto_mod_rules: typing.Dict[snowflakes.Snowflake, auto_mod_models.AutoModRule] = {} + for rule_payload in payload["auto_moderation_rules"]: + try: + rule = self.deserialize_auto_mod_rule(rule_payload) + + except errors.UnrecognisedEntityError: + continue + + auto_mod_rules[rule.id] = rule + integrations = { snowflakes.Snowflake(integration["id"]): self.deserialize_partial_integration(integration) for integration in payload["integrations"] @@ -922,7 +946,12 @@ def deserialize_audit_log( webhooks[webhook.id] = webhook return audit_log_models.AuditLog( - entries=entries, integrations=integrations, threads=threads, users=users, webhooks=webhooks + auto_mod_rules=auto_mod_rules, + entries=entries, + integrations=integrations, + threads=threads, + users=users, + webhooks=webhooks, ) ################## @@ -3683,3 +3712,96 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhook_model _LOGGER.debug(f"Unrecognised webhook type {webhook_type}") raise errors.UnrecognisedEntityError(f"Unrecognised webhook type {webhook_type}") + + ################### + # AUTO-MOD MODELS # + ################### + + def _deserialize_auto_mod_block_message( + self, payload: data_binding.JSONObject + ) -> auto_mod_models.AutoModBlockMessage: + return auto_mod_models.AutoModBlockMessage(type=auto_mod_models.AutoModActionType(payload["type"])) + + def _deserialize_auto_mod_block_send_alert_message( + self, payload: data_binding.JSONObject + ) -> auto_mod_models.AutoModSendAlertMessage: + return auto_mod_models.AutoModSendAlertMessage( + channel_id=snowflakes.Snowflake(payload["metadata"]["channel_id"]), + type=auto_mod_models.AutoModActionType(payload["type"]), + ) + + def _deserialize_auto_mod_timeout(self, payload: data_binding.JSONObject) -> auto_mod_models.AutoModTimeout: + return auto_mod_models.AutoModTimeout( + type=auto_mod_models.AutoModActionType(payload["type"]), + duration=datetime.timedelta(seconds=payload["metadata"]["duration_seconds"]), + ) + + def deserialize_auto_mod_action(self, payload: data_binding.JSONObject) -> auto_mod_models.PartialAutoModAction: + action_type = auto_mod_models.AutoModActionType(payload["type"]) + + if converter := self._auto_mod_action_mapping.get(action_type): + return converter(payload) + + _LOGGER.debug(f"Unrecognised auto-moderation action type {action_type}") + raise errors.UnrecognisedEntityError(f"Unrecognised auto-moderation action type {action_type}") + + def serialize_auto_mod_action(self, action: auto_mod_models.PartialAutoModAction) -> data_binding.JSONObject: + payload: typing.Dict[str, typing.Any] = {"type": action.type} + + # TODO: can we switch to using the type attribute here for efficiency? + if isinstance(action, auto_mod_models.AutoModSendAlertMessage): + payload["metadata"] = {"channel_id": str(action.channel_id)} + + elif isinstance(action, auto_mod_models.AutoModTimeout): + payload["metadata"] = {"duration_seconds": int(action.duration.total_seconds())} + + return payload + + def _deserialize_auto_mod_harmful_link_trigger( + self, _: typing.Optional[data_binding.JSONObject], / + ) -> auto_mod_models.HarmfulLinkTrigger: + return auto_mod_models.HarmfulLinkTrigger(type=auto_mod_models.AutoModTriggerType.HARMFUL_LINK) + + def _deserialize_auto_mod_keyword_trigger( + self, payload: typing.Optional[data_binding.JSONObject], / + ) -> auto_mod_models.KeywordTrigger: + assert payload is not None + return auto_mod_models.KeywordTrigger( + type=auto_mod_models.AutoModTriggerType.KEYWORD, keyword_filter=payload["keyword_filter"] + ) + + def _deserialize_auto_mod_keyword_preset_trigger( + self, payload: typing.Optional[data_binding.JSONObject], / + ) -> auto_mod_models.KeywordPresetTrigger: + assert payload is not None + return auto_mod_models.KeywordPresetTrigger( + type=auto_mod_models.AutoModTriggerType.KEYWORD_PRESET, + allow_list=payload["allow_list"], + presets=[auto_mod_models.AutoModKeywordPresetType(preset) for preset in payload["presets"]], + ) + + def _deserialize_auto_mod_spam_trigger( + self, _: typing.Optional[data_binding.JSONObject], / + ) -> auto_mod_models.SpamTrigger: + return auto_mod_models.SpamTrigger(type=auto_mod_models.AutoModTriggerType.SPAM) + + def deserialize_auto_mod_rule(self, payload: data_binding.JSONObject) -> auto_mod_models.AutoModRule: + trigger_type = auto_mod_models.AutoModTriggerType(payload["trigger_type"]) + trigger_converter = self._auto_mod_trigger_mapping.get(trigger_type) + if not trigger_converter: + _LOGGER.debug(f"Unrecognised auto-moderation trigger type {trigger_type}") + raise errors.UnrecognisedEntityError(f"Unrecognised auto-moderation trigger type {trigger_type}") + + return auto_mod_models.AutoModRule( + app=self._app, + id=snowflakes.Snowflake(payload["id"]), + guild_id=snowflakes.Snowflake(payload["guild_id"]), + name=payload["name"], + creator_id=snowflakes.Snowflake(payload["creator_id"]), + event_type=auto_mod_models.AutoModEventType(payload["event_type"]), + trigger=trigger_converter(payload.get("trigger_metadata")), + actions=[self.deserialize_auto_mod_action(action) for action in payload["actions"]], + is_enabled=payload["enabled"], + exempt_channel_ids=[snowflakes.Snowflake(id_) for id_ in payload["exempt_channels"]], + exempt_role_ids=[snowflakes.Snowflake(id_) for id_ in payload["exempt_roles"]], + ) diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py index 0eb69e5179..7919f429a1 100644 --- a/hikari/impl/event_factory.py +++ b/hikari/impl/event_factory.py @@ -31,6 +31,7 @@ import typing from hikari import applications as application_models +from hikari import auto_mod as auto_mod_models from hikari import channels as channel_models from hikari import colors from hikari import emojis as emojis_models @@ -39,6 +40,7 @@ from hikari import users as user_models from hikari.api import event_factory from hikari.events import application_events +from hikari.events import auto_mod_events from hikari.events import channel_events from hikari.events import guild_events from hikari.events import interaction_events @@ -963,3 +965,46 @@ def deserialize_voice_server_update_event( return voice_events.VoiceServerUpdateEvent( app=self._app, shard=shard, guild_id=guild_id, token=token, raw_endpoint=raw_endpoint ) + + def deserialize_auto_mod_rule_create_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> auto_mod_events.AutoModRuleCreateEvent: + return auto_mod_events.AutoModRuleCreateEvent( + shard=shard, rule=self._app.entity_factory.deserialize_auto_mod_rule(payload) + ) + + def deserialize_auto_mod_rule_update_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> auto_mod_events.AutoModRuleUpdateEvent: + return auto_mod_events.AutoModRuleUpdateEvent( + shard=shard, rule=self._app.entity_factory.deserialize_auto_mod_rule(payload) + ) + + def deserialize_auto_mod_rule_delete_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> auto_mod_events.AutoModRuleDeleteEvent: + return auto_mod_events.AutoModRuleDeleteEvent( + shard=shard, rule=self._app.entity_factory.deserialize_auto_mod_rule(payload) + ) + + def deserialize_auto_mod_action_execution_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> auto_mod_events.AutoModActionExecutionEvent: + channel_id = payload.get("channel_id") + message_id = payload.get("message_id") + alert_message_id = payload.get("alert_system_message_id") + return auto_mod_events.AutoModActionExecutionEvent( + app=self._app, + shard=shard, + guild_id=snowflakes.Snowflake(payload["guild_id"]), + action=self._app.entity_factory.deserialize_auto_mod_action(payload["action"]), + rule_id=snowflakes.Snowflake(payload["rule_id"]), + rule_trigger_type=auto_mod_models.AutoModTriggerType(payload["rule_trigger_type"]), + user_id=snowflakes.Snowflake(payload["user_id"]), + channel_id=snowflakes.Snowflake(channel_id) if channel_id is not None else None, + message_id=snowflakes.Snowflake(message_id) if message_id is not None else None, + alert_system_message_id=snowflakes.Snowflake(alert_message_id) if alert_message_id is not None else None, + content=payload.get("content") or None, + matched_keyword=payload["matched_keyword"], + matched_content=payload.get("matched_content") or None, + ) diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index ef1c659053..22cb2ecbf8 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -38,6 +38,7 @@ from hikari import snowflakes from hikari.api import config from hikari.events import application_events +from hikari.events import auto_mod_events from hikari.events import channel_events from hikari.events import guild_events from hikari.events import interaction_events @@ -845,6 +846,34 @@ async def on_guild_scheduled_event_user_remove( """See https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-user-remove for more info.""" await self.dispatch(self._event_factory.deserialize_scheduled_event_user_remove_event(shard, payload)) + @event_manager_base.filtered(auto_mod_events.AutoModRuleCreateEvent) + async def on_auto_moderation_rule_create( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> None: + """See https://discord.com/developers/docs/topics/gateway#auto-moderation-rule-create for more info.""" + await self.dispatch(self._event_factory.deserialize_auto_mod_rule_create_event(shard, payload)) + + @event_manager_base.filtered(auto_mod_events.AutoModRuleUpdateEvent) + async def on_auto_moderation_rule_update( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> None: + """See https://discord.com/developers/docs/topics/gateway#auto-moderation-rule-update for more info.""" + await self.dispatch(self._event_factory.deserialize_auto_mod_rule_update_event(shard, payload)) + + @event_manager_base.filtered(auto_mod_events.AutoModRuleDeleteEvent) + async def on_auto_moderation_rule_delete( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> None: + """See https://discord.com/developers/docs/topics/gateway#auto-moderation-rule-delete for more info.""" + await self.dispatch(self._event_factory.deserialize_auto_mod_rule_delete_event(shard, payload)) + + @event_manager_base.filtered(auto_mod_events.AutoModActionExecutionEvent) + async def on_auto_moderation_action_execution( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> None: + """See https://discord.com/developers/docs/topics/gateway#auto-moderation-action-execution for more info.""" + await self.dispatch(self._event_factory.deserialize_auto_mod_action_execution_event(shard, payload)) + @event_manager_base.filtered(guild_events.AuditLogEntryCreateEvent) async def on_guild_audit_log_entry_create( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 5e1b42d31e..c6d675d686 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -86,6 +86,7 @@ import types from hikari import audit_logs + from hikari import auto_mod from hikari import invites from hikari import sessions from hikari import stickers @@ -4361,3 +4362,111 @@ def fetch_scheduled_event_users( return special_endpoints_impl.ScheduledEventUserIterator( self._entity_factory, self._request, newest_first, str(start_at), guild, event ) + + async def fetch_auto_mod_rules( + self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], / + ) -> typing.Sequence[auto_mod.AutoModRule]: + results = await self._request(routes.GET_GUILD_AUTO_MODERATION_RULES.compile(guild=guild)) + assert isinstance(results, list) + return [self._entity_factory.deserialize_auto_mod_rule(rule) for rule in results] + + async def fetch_auto_mod_rule( + self, + guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], + rule: snowflakes.SnowflakeishOr[auto_mod.AutoModRule], + /, + ) -> auto_mod.AutoModRule: + result = await self._request(routes.GET_GUILD_AUTO_MODERATION_RULE.compile(guild=guild, rule=rule)) + assert isinstance(result, dict) + return self._entity_factory.deserialize_auto_mod_rule(result) + + async def create_auto_mod_rule( + self, + guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], + /, + name: str, + event_type: typing.Union[int, auto_mod.AutoModEventType], + trigger_type: typing.Union[int, auto_mod.AutoModTriggerType], + actions: typing.Sequence[auto_mod.PartialAutoModAction], + allow_list: undefined.UndefinedOr[typing.Sequence[str]] = undefined.UNDEFINED, + keyword_filter: undefined.UndefinedOr[typing.Sequence[str]] = undefined.UNDEFINED, + presets: undefined.UndefinedOr[ + typing.Sequence[typing.Union[int, auto_mod.AutoModKeywordPresetType]] + ] = undefined.UNDEFINED, + enabled: undefined.UndefinedOr[bool] = True, + exempt_channels: undefined.UndefinedOr[ + snowflakes.SnowflakeishSequence[channels_.PartialChannel] + ] = undefined.UNDEFINED, + exempt_roles: undefined.UndefinedOr[snowflakes.SnowflakeishSequence[guilds.PartialRole]] = undefined.UNDEFINED, + reason: undefined.UndefinedOr[str] = undefined.UNDEFINED, + ) -> auto_mod.AutoModRule: + route = routes.POST_GUILD_AUTO_MODERATION_RULE.compile(guild=guild) + payload = data_binding.JSONObjectBuilder() + payload.put("name", name) + payload.put("event_type", event_type) + payload.put("trigger_type", trigger_type) + payload.put("enabled", enabled) + payload.put_snowflake_array("exempt_channels", exempt_channels) + payload.put_snowflake_array("exempt_roles", exempt_roles) + + payload.put_array("actions", actions, conversion=self._entity_factory.serialize_auto_mod_action) + + if not undefined.all_undefined(allow_list, keyword_filter, presets): + trigger_metadata = data_binding.JSONObjectBuilder() + trigger_metadata.put("allow_list", allow_list) + trigger_metadata.put("keyword_filter", keyword_filter) + trigger_metadata.put("presets", presets) + payload.put("trigger_metadata", trigger_metadata) + + result = await self._request(route, json=payload, reason=reason) + assert isinstance(result, dict) + return self._entity_factory.deserialize_auto_mod_rule(result) + + async def edit_auto_mod_rule( + self, + guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], + rule: snowflakes.SnowflakeishOr[auto_mod.AutoModRule], + /, + name: undefined.UndefinedOr[str] = undefined.UNDEFINED, + event_type: undefined.UndefinedOr[typing.Union[int, auto_mod.AutoModEventType]] = undefined.UNDEFINED, + actions: undefined.UndefinedOr[typing.Sequence[auto_mod.PartialAutoModAction]] = undefined.UNDEFINED, + allow_list: undefined.UndefinedOr[typing.Sequence[str]] = undefined.UNDEFINED, + keyword_filter: undefined.UndefinedOr[typing.Sequence[str]] = undefined.UNDEFINED, + presets: undefined.UndefinedOr[ + typing.Sequence[typing.Union[int, auto_mod.AutoModKeywordPresetType]] + ] = undefined.UNDEFINED, + enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + exempt_channels: undefined.UndefinedOr[ + snowflakes.SnowflakeishSequence[channels_.PartialChannel] + ] = undefined.UNDEFINED, + exempt_roles: undefined.UndefinedOr[snowflakes.SnowflakeishSequence[guilds.PartialRole]] = undefined.UNDEFINED, + reason: undefined.UndefinedOr[str] = undefined.UNDEFINED, + ) -> auto_mod.AutoModRule: + route = routes.PATCH_GUILD_AUTO_MODERATION_RULE.compile(guild=guild, rule=rule) + payload = data_binding.JSONObjectBuilder() + payload.put("name", name) + payload.put("event_type", event_type) + payload.put("enabled", enabled) + payload.put_snowflake_array("exempt_channels", exempt_channels) + payload.put_snowflake_array("exempt_roles", exempt_roles) + payload.put_array("actions", actions, conversion=self._entity_factory.serialize_auto_mod_action) + + if not undefined.all_undefined(allow_list, keyword_filter, presets): + trigger_metadata = data_binding.JSONObjectBuilder() + trigger_metadata.put("allow_list", allow_list) + trigger_metadata.put("keyword_filter", keyword_filter) + trigger_metadata.put("presets", presets) + payload.put("trigger_metadata", trigger_metadata) + + result = await self._request(route, json=payload, reason=reason) + assert isinstance(result, dict) + return self._entity_factory.deserialize_auto_mod_rule(result) + + async def delete_auto_mod_rule( + self, + guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], + rule: snowflakes.SnowflakeishOr[auto_mod.AutoModRule], + /, + reason: undefined.UndefinedOr[str] = undefined.UNDEFINED, + ) -> None: + await self._request(routes.DELETE_GUILD_AUTO_MODERATION_RULE.compile(guild=guild, rule=rule), reason=reason) diff --git a/hikari/intents.py b/hikari/intents.py index 19591ac942..69be0361a3 100644 --- a/hikari/intents.py +++ b/hikari/intents.py @@ -335,6 +335,20 @@ class Intents(enums.Flag): * `GUILD_SCHEDULED_EVENT_USER_REMOVE` """ + AUTO_MODERATION_CONFIGURATION = 1 << 20 + """Subscribe to the following events: + + * `AUTO_MODERATION_RULE_CREATE` + * `AUTO_MODERATION_RULE_UPDATE` + * `AUTO_MODERATION_RULE_DELETE` + """ + + AUTO_MODERATION_EXECUTION = 1 << 21 + """Subscribe to the following events: + + * `AUTO_MODERATION_ACTION_EXECUTION` + """ + # Annoyingly, enums hide classmethods and staticmethods from __dir__ in # EnumMeta which means if I make methods to generate these, then stuff # will not be documented by pdoc. Alas, my dream of being smart with @@ -386,7 +400,7 @@ class Intents(enums.Flag): ALL_MESSAGE_TYPING = DM_MESSAGE_TYPING | GUILD_MESSAGE_TYPING """All typing indicator intents.""" - ALL_UNPRIVILEGED = ALL_GUILDS_UNPRIVILEGED | ALL_DMS + ALL_UNPRIVILEGED = ALL_GUILDS_UNPRIVILEGED | ALL_DMS | AUTO_MODERATION_CONFIGURATION | AUTO_MODERATION_EXECUTION """All unprivileged intents.""" ALL_PRIVILEGED = ALL_GUILDS_PRIVILEGED | MESSAGE_CONTENT diff --git a/hikari/internal/routes.py b/hikari/internal/routes.py index 9b70248f88..ef0327ac1a 100644 --- a/hikari/internal/routes.py +++ b/hikari/internal/routes.py @@ -447,6 +447,12 @@ def compile_to_file( GET_GUILD_WEBHOOKS: typing.Final[Route] = Route(GET, "/guilds/{guild}/webhooks") +GET_GUILD_AUTO_MODERATION_RULES: typing.Final[Route] = Route(GET, "/guilds/{guild}/auto-moderation/rules") +GET_GUILD_AUTO_MODERATION_RULE: typing.Final[Route] = Route(GET, "/guilds/{guild}/auto-moderation/rules/{rule}") +POST_GUILD_AUTO_MODERATION_RULE: typing.Final[Route] = Route(POST, "/guilds/{guild}/auto-moderation/rules") +PATCH_GUILD_AUTO_MODERATION_RULE: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/auto-moderation/rules/{rule}") +DELETE_GUILD_AUTO_MODERATION_RULE: typing.Final[Route] = Route(DELETE, "/guilds/{guild}/auto-moderation/rules/{rule}") + # Stickers GET_STICKER_PACKS: typing.Final[Route] = Route(GET, "/sticker-packs") GET_STICKER: typing.Final[Route] = Route(GET, "/stickers/{sticker}") diff --git a/hikari/invites.py b/hikari/invites.py index 4423d060c7..8803551cf3 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -102,7 +102,7 @@ class InviteGuild(guilds.PartialGuild): """Represents the partial data of a guild that is attached to invites.""" features: typing.Sequence[typing.Union[str, guilds.GuildFeature]] = attr.field(eq=False, hash=False, repr=False) - """A list of the features in this guild.""" + """A sequence of the features in this guild.""" splash_hash: typing.Optional[str] = attr.field(eq=False, hash=False, repr=False) """The hash of the splash for the guild, if there is one.""" diff --git a/hikari/messages.py b/hikari/messages.py index 02a9eef172..65d7dfd079 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -133,6 +133,9 @@ class MessageType(int, enums.Enum): CONTEXT_MENU_COMMAND = 23 """A message sent to indicate a context menu has been executed.""" + AUTO_MODERATION_ACTION = 24 + """A message sent to indicate an auto-moderation action has been triggered.""" + @typing.final class MessageFlag(enums.Flag): diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 54e937fdc9..b1f3da2143 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -27,6 +27,7 @@ from hikari import applications as application_models from hikari import audit_logs as audit_log_models +from hikari import auto_mod from hikari import channels as channel_models from hikari import colors as color_models from hikari import commands @@ -1455,7 +1456,17 @@ def partial_integration_payload(self): "account": {"id": "543453", "name": "Blam"}, } - def test_deserialize_audit_log_entry(self, entity_factory_impl, audit_log_entry_payload, mock_app): + def test_deserialize_audit_log_entry( + self, + entity_factory_impl, + auto_mod_rule_payload, + audit_log_entry_payload, + application_webhook_payload, + incoming_webhook_payload, + follower_webhook_payload, + partial_integration_payload, + mock_app, + ): entry = entity_factory_impl.deserialize_audit_log_entry( audit_log_entry_payload, guild_id=snowflakes.Snowflake(123321) ) @@ -1487,6 +1498,24 @@ def test_deserialize_audit_log_entry(self, entity_factory_impl, audit_log_entry_ role.id == 123123123312312 role.name == "aRole" + assert audit_log_models.auto_mod_rules == { + 94594949494: entity_factory_impl.deserialize_auto_mod_rule(auto_mod_rule_payload) + } + assert audit_log_models.integrations == { + 4949494949: entity_factory_impl.deserialize_partial_integration(partial_integration_payload) + } + assert audit_log_models.threads == { + 947643783913308301: entity_factory_impl.deserialize_guild_public_thread(guild_public_thread_payload), + 947690637610844210: entity_factory_impl.deserialize_guild_private_thread(guild_private_thread_payload), + 946900871160164393: entity_factory_impl.deserialize_guild_news_thread(guild_news_thread_payload), + } + assert audit_log_models.users == {115590097100865541: entity_factory_impl.deserialize_user(user_payload)} + assert audit_log_models.webhooks == { + 223704706495545344: entity_factory_impl.deserialize_incoming_webhook(incoming_webhook_payload), + 658822586720976555: entity_factory_impl.deserialize_application_webhook(application_webhook_payload), + 752831914402115456: entity_factory_impl.deserialize_channel_follower_webhook(follower_webhook_payload), + } + def test_deserialize_audit_log_entry_when_guild_id_in_payload( self, entity_factory_impl, audit_log_entry_payload, mock_app ): @@ -1560,6 +1589,7 @@ def test_deserialize_audit_log_entry_for_unknown_action_type(self, entity_factor def audit_log_payload( self, audit_log_entry_payload, + auto_mod_rule_payload, user_payload, incoming_webhook_payload, application_webhook_payload, @@ -1571,6 +1601,7 @@ def audit_log_payload( ): return { "audit_log_entries": [audit_log_entry_payload], + "auto_moderation_rules": [auto_mod_rule_payload], "integrations": [partial_integration_payload], "threads": [guild_public_thread_payload, guild_private_thread_payload, guild_news_thread_payload], "users": [user_payload], @@ -1632,6 +1663,7 @@ def test_deserialize_audit_log_skips_unknown_webhook_type( ): audit_log = entity_factory_impl.deserialize_audit_log( { + "auto_moderation_rules": [], "webhooks": [incoming_webhook_payload, {"type": -99999}, application_webhook_payload], "threads": [], "users": [], @@ -1668,6 +1700,21 @@ def test_deserialize_audit_log_skips_unknown_thread_type( 947690637610844210: entity_factory_impl.deserialize_guild_private_thread(guild_private_thread_payload), } + def test_deserialize_audit_log_skips_unknown_auto_mod_rule_type(self, entity_factory_impl, auto_mod_rule_payload): + audit_log = entity_factory_impl.deserialize_audit_log( + { + "auto_moderation_rules": [{"id": "4949", "trigger_type": -6959595}, auto_mod_rule_payload], + "webhooks": [], + "users": [], + "audit_log_entries": [], + "integrations": [], + } + ) + + assert audit_log.auto_mod_rules == { + 94594949494: entity_factory_impl.deserialize_auto_mod_rule(auto_mod_rule_payload) + } + ################## # CHANNEL MODELS # ################## @@ -7107,3 +7154,148 @@ def test_deserialize_webhook(self, mock_app, type_, fn): def test_deserialize_webhook_for_unexpected_webhook_type(self, entity_factory_impl): with pytest.raises(errors.UnrecognisedEntityError): entity_factory_impl.deserialize_webhook({"type": -7999}) + + def test_deserialize_auto_mod_action_for_block_messages(self, entity_factory_impl): + result = entity_factory_impl.deserialize_auto_mod_action({"type": 1}) + + assert result.type is auto_mod.AutoModActionType.BLOCK_MESSAGES + assert isinstance(result, auto_mod.AutoModBlockMessage) + + def test_deserialize_auto_mod_action_for_send_alert_message(self, entity_factory_impl): + result = entity_factory_impl.deserialize_auto_mod_action({"type": 2, "metadata": {"channel_id": "43123123"}}) + + assert result.type is auto_mod.AutoModActionType.SEND_ALERT_MESSAGE + assert isinstance(result, auto_mod.AutoModSendAlertMessage) + assert result.channel_id == 43123123 + + def test_deserialize_auto_mod_action_for_timeout(self, entity_factory_impl): + result = entity_factory_impl.deserialize_auto_mod_action({"type": 3, "metadata": {"duration_seconds": 123321}}) + + assert result.type is auto_mod.AutoModActionType.TIMEOUT + assert isinstance(result, auto_mod.AutoModTimeout) + assert result.duration == datetime.timedelta(seconds=123321) + + def test_deserialize_auto_mod_action_for_unknown_type(self, entity_factory_impl): + with pytest.raises(errors.UnrecognisedEntityError): + entity_factory_impl.deserialize_auto_mod_action({"type": -696969}) + + def test_serialize_auto_mod_action(self, entity_factory_impl): + result = entity_factory_impl.serialize_auto_mod_action( + auto_mod.AutoModBlockMessage(type=auto_mod.AutoModActionType.BLOCK_MESSAGES) + ) + + assert result == {"type": 1} + + def test_serialize_auto_mod_action_for_send_alert_message(self, entity_factory_impl): + result = entity_factory_impl.serialize_auto_mod_action( + auto_mod.AutoModSendAlertMessage(type=auto_mod.AutoModActionType.SEND_ALERT_MESSAGE, channel_id=534123321) + ) + + assert result == {"type": 2, "metadata": {"channel_id": "534123321"}} + + def test_serialize_auto_mod_action_for_timeout(self, entity_factory_impl): + result = entity_factory_impl.serialize_auto_mod_action( + auto_mod.AutoModTimeout( + type=auto_mod.AutoModActionType.TIMEOUT, duration=datetime.timedelta(seconds=123321) + ) + ) + + assert result == {"type": 3, "metadata": {"duration_seconds": 123321}} + + @pytest.fixture() + def auto_mod_rule_payload(self): + return { + "id": "94594949494", + "guild_id": "9595939234", + "name": "hihihihi", + "creator_id": "595684849", + "event_type": 1, + "trigger_type": 4, + "trigger_metadata": {"presets": [1, 2, 3], "allow_list": ["okokok", "No"]}, + "actions": [ + {"type": 1}, + {"type": 2, "metadata": {"channel_id": "43212123"}}, + {"type": 3, "metadata": {"duration_seconds": 321123}}, + ], + "enabled": True, + "exempt_roles": ["49493932", "123321"], + "exempt_channels": ["95959595", "31223"], + } + + def test_deserialize_auto_mod_rule(self, entity_factory_impl, auto_mod_rule_payload): + result = entity_factory_impl.deserialize_auto_mod_rule(auto_mod_rule_payload) + + assert result.id == 94594949494 + assert result.guild_id == 9595939234 + assert result.name == "hihihihi" + assert result.creator_id == 595684849 + assert result.event_type is auto_mod.AutoModEventType.MESSAGE_SEND + assert isinstance(result.trigger, auto_mod.KeywordPresetTrigger) + assert result.trigger.type is auto_mod.AutoModTriggerType.KEYWORD_PRESET + assert result.actions == [ + entity_factory_impl.deserialize_auto_mod_action({"type": 1}), + entity_factory_impl.deserialize_auto_mod_action({"type": 2, "metadata": {"channel_id": "43212123"}}), + entity_factory_impl.deserialize_auto_mod_action({"type": 3, "metadata": {"duration_seconds": 321123}}), + ] + assert result.is_enabled is True + assert result.exempt_role_ids == [49493932, 123321] + assert result.exempt_channel_ids == [95959595, 31223] + + def test_deserialize_auto_mod_rule_for_keyword_trigger(self, entity_factory_impl, auto_mod_rule_payload): + result = entity_factory_impl.deserialize_auto_mod_rule( + { + "id": "94594949494", + "guild_id": "9595939234", + "name": "hihihihi", + "creator_id": "595684849", + "event_type": 1, + "trigger_type": 1, + "trigger_metadata": {"keyword_filter": ["ok", "no", "bye"]}, + "actions": [], + "enabled": True, + "exempt_roles": [], + "exempt_channels": [], + } + ) + + assert isinstance(result.trigger, auto_mod.KeywordTrigger) + assert result.trigger.type is auto_mod.AutoModTriggerType.KEYWORD + assert result.trigger.keyword_filter == ["ok", "no", "bye"] + + def test_deserialize_auto_mod_rule_for_harmful_link_trigger(self, entity_factory_impl, auto_mod_rule_payload): + result = entity_factory_impl.deserialize_auto_mod_rule( + { + "id": "94594949494", + "guild_id": "9595939234", + "name": "hihihihi", + "creator_id": "595684849", + "event_type": 1, + "trigger_type": 2, + "actions": [], + "enabled": True, + "exempt_roles": [], + "exempt_channels": [], + } + ) + + assert isinstance(result.trigger, auto_mod.HarmfulLinkTrigger) + assert result.trigger.type is auto_mod.AutoModTriggerType.HARMFUL_LINK + + def test_deserialize_auto_mod_rule_for_spam_trigger(self, entity_factory_impl, auto_mod_rule_payload): + result = entity_factory_impl.deserialize_auto_mod_rule( + { + "id": "94594949494", + "guild_id": "9595939234", + "name": "hihihihi", + "creator_id": "595684849", + "event_type": 1, + "trigger_type": 3, + "actions": [], + "enabled": True, + "exempt_roles": [], + "exempt_channels": [], + } + ) + + assert isinstance(result.trigger, auto_mod.SpamTrigger) + assert result.trigger.type is auto_mod.AutoModTriggerType.SPAM diff --git a/tests/hikari/impl/test_event_factory.py b/tests/hikari/impl/test_event_factory.py index 01e6c8e06f..67750dc0b0 100644 --- a/tests/hikari/impl/test_event_factory.py +++ b/tests/hikari/impl/test_event_factory.py @@ -24,6 +24,7 @@ import mock import pytest +from hikari import auto_mod from hikari import channels as channel_models from hikari import emojis as emoji_models from hikari import traits @@ -31,6 +32,7 @@ from hikari import users as user_models from hikari.api import shard from hikari.events import application_events +from hikari.events import auto_mod_events from hikari.events import channel_events from hikari.events import guild_events from hikari.events import interaction_events @@ -1495,3 +1497,100 @@ def test_deserialize_voice_server_update_event(self, event_factory, mock_app, mo assert event.token == "okokok" assert event.guild_id == 3122312 assert event.raw_endpoint == "httppppppp" + + def test_deserialize_auto_mod_rule_create_event(self, event_factory, mock_app, mock_shard): + mock_payload = {"id": "49499494"} + + event = event_factory.deserialize_auto_mod_rule_create_event(mock_shard, mock_payload) + + assert isinstance(event, auto_mod_events.AutoModRuleCreateEvent) + assert event.shard is mock_shard + assert event.rule is mock_app.entity_factory.deserialize_auto_mod_rule.return_value + mock_app.entity_factory.deserialize_auto_mod_rule.assert_called_once_with(mock_payload) + + def test_deserialize_auto_mod_rule_update_event(self, event_factory, mock_app, mock_shard): + mock_payload = {"id": "49499494"} + + event = event_factory.deserialize_auto_mod_rule_update_event(mock_shard, mock_payload) + + assert isinstance(event, auto_mod_events.AutoModRuleUpdateEvent) + assert event.shard is mock_shard + assert event.rule is mock_app.entity_factory.deserialize_auto_mod_rule.return_value + mock_app.entity_factory.deserialize_auto_mod_rule.assert_called_once_with(mock_payload) + + def test_deserialize_auto_mod_rule_delete_event(self, event_factory, mock_app, mock_shard): + mock_payload = {"id": "49499494"} + + event = event_factory.deserialize_auto_mod_rule_delete_event(mock_shard, mock_payload) + + assert isinstance(event, auto_mod_events.AutoModRuleDeleteEvent) + assert event.shard is mock_shard + assert event.rule is mock_app.entity_factory.deserialize_auto_mod_rule.return_value + mock_app.entity_factory.deserialize_auto_mod_rule.assert_called_once_with(mock_payload) + + def test_deserialize_auto_mod_action_execution_event(self, event_factory, mock_app, mock_shard): + mock_action_payload = {"type": "69"} + + event = event_factory.deserialize_auto_mod_action_execution_event( + mock_shard, + { + "guild_id": "123321", + "action": mock_action_payload, + "rule_id": "4959595", + "rule_trigger_type": 3, + "user_id": "4949494", + "channel_id": "5423234", + "message_id": "49343292", + "alert_system_message_id": "49211123", + "content": "meow", + "matched_keyword": "fredf", + "matched_content": "dfofodofdodf", + }, + ) + + assert event.app is mock_app + assert event.shard is mock_shard + assert event.guild_id == 123321 + assert event.action is mock_app.entity_factory.deserialize_auto_mod_action.return_value + assert event.rule_id == 4959595 + assert event.rule_trigger_type is auto_mod.AutoModTriggerType.SPAM + assert event.user_id == 4949494 + assert event.channel_id == 5423234 + assert event.message_id == 49343292 + assert event.alert_system_message_id == 49211123 + assert event.content == "meow" + assert event.matched_keyword == "fredf" + assert event.matched_content == "dfofodofdodf" + mock_app.entity_factory.deserialize_auto_mod_action.assert_called_once_with(mock_action_payload) + + def test_deserialize_auto_mod_action_execution_event_when_partial(self, event_factory, mock_app, mock_shard): + mock_action_payload = {"type": "69"} + + event = event_factory.deserialize_auto_mod_action_execution_event( + mock_shard, + { + "guild_id": "123321", + "action": mock_action_payload, + "rule_id": "4959595", + "rule_trigger_type": 3, + "user_id": "4949494", + "content": "", + "matched_keyword": None, + "matched_content": None, + }, + ) + + assert event.app is mock_app + assert event.shard is mock_shard + assert event.guild_id == 123321 + assert event.action is mock_app.entity_factory.deserialize_auto_mod_action.return_value + assert event.rule_id == 4959595 + assert event.rule_trigger_type is auto_mod.AutoModTriggerType.SPAM + assert event.user_id == 4949494 + assert event.channel_id is None + assert event.message_id is None + assert event.alert_system_message_id is None + assert event.content is None + assert event.matched_keyword is None + assert event.matched_content is None + mock_app.entity_factory.deserialize_auto_mod_action.assert_called_once_with(mock_action_payload) diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py index 5d1514d43c..fbea7f41f9 100644 --- a/tests/hikari/impl/test_event_manager.py +++ b/tests/hikari/impl/test_event_manager.py @@ -1691,6 +1691,70 @@ async def test_on_guild_scheduled_event_user_remove( event_factory.deserialize_scheduled_event_user_remove_event.return_value ) + @pytest.mark.asyncio() + async def test_on_auto_moderation_rule_create( + self, + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, + ): + mock_payload = mock.Mock() + + await event_manager_impl.on_auto_moderation_rule_create(shard, mock_payload) + + event_factory.deserialize_auto_mod_rule_create_event.assert_called_once_with(shard, mock_payload) + event_manager_impl.dispatch.assert_awaited_once_with( + event_factory.deserialize_auto_mod_rule_create_event.return_value + ) + + @pytest.mark.asyncio() + async def test_on_auto_moderation_rule_update( + self, + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, + ): + mock_payload = mock.Mock() + + await event_manager_impl.on_auto_moderation_rule_update(shard, mock_payload) + + event_factory.deserialize_auto_mod_rule_update_event.assert_called_once_with(shard, mock_payload) + event_manager_impl.dispatch.assert_awaited_once_with( + event_factory.deserialize_auto_mod_rule_update_event.return_value + ) + + @pytest.mark.asyncio() + async def test_on_auto_moderation_rule_delete( + self, + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, + ): + mock_payload = mock.Mock() + + await event_manager_impl.on_auto_moderation_rule_delete(shard, mock_payload) + + event_factory.deserialize_auto_mod_rule_delete_event.assert_called_once_with(shard, mock_payload) + event_manager_impl.dispatch.assert_awaited_once_with( + event_factory.deserialize_auto_mod_rule_delete_event.return_value + ) + + @pytest.mark.asyncio() + async def test_on_auto_moderation_action_execution( + self, + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, + ): + mock_payload = mock.Mock() + + await event_manager_impl.on_auto_moderation_action_execution(shard, mock_payload) + + event_factory.deserialize_auto_mod_action_execution_event.assert_called_once_with(shard, mock_payload) + event_manager_impl.dispatch.assert_awaited_once_with( + event_factory.deserialize_auto_mod_action_execution_event.return_value + ) + @pytest.mark.asyncio() async def test_on_guild_audit_log_entry_create( self, diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 1a062e5d67..231621b43a 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -31,6 +31,7 @@ from hikari import applications from hikari import audit_logs +from hikari import auto_mod from hikari import channels from hikari import colors from hikari import commands @@ -6514,3 +6515,164 @@ async def test_delete_scheduled_event(self, rest_client: rest.RESTClientImpl): await rest_client.delete_scheduled_event(StubModel(54531123), StubModel(123321123321)) rest_client._request.assert_awaited_once_with(expected_route) + + async def test_fetch_auto_mod_rules(self, rest_client: rest.RESTClientImpl): + mock_payload_1 = {"id": "432123"} + mock_payload_2 = {"id": "949494994"} + mock_result_1 = mock.Mock() + mock_result_2 = mock.Mock() + rest_client._entity_factory.deserialize_auto_mod_rule.side_effect = [mock_result_1, mock_result_2] + expected_route = routes.GET_GUILD_AUTO_MODERATION_RULES.compile(guild=123321) + rest_client._request = mock.AsyncMock(return_value=[mock_payload_1, mock_payload_2]) + + result = await rest_client.fetch_auto_mod_rules(StubModel(123321)) + + assert result == [mock_result_1, mock_result_2] + rest_client._request.assert_awaited_once_with(expected_route) + rest_client._entity_factory.deserialize_auto_mod_rule.assert_has_calls( + [mock.call(mock_payload_1), mock.call(mock_payload_2)] + ) + + async def test_fetch_auto_mod_rule(self, rest_client: rest.RESTClientImpl): + expected_route = routes.GET_GUILD_AUTO_MODERATION_RULE.compile(guild=123321, rule=5443123) + rest_client._request = mock.AsyncMock(return_value={"id": "442123"}) + + result = await rest_client.fetch_auto_mod_rule(StubModel(123321), StubModel(5443123)) + + assert result is rest_client._entity_factory.deserialize_auto_mod_rule.return_value + rest_client._request.assert_awaited_once_with(expected_route) + rest_client._entity_factory.deserialize_auto_mod_rule.assert_called_once_with(rest_client._request.return_value) + + async def test_create_auto_mod_rule(self, rest_client: rest.RESTClientImpl): + mock_action = {"type": "58585858"} + expected_route = routes.POST_GUILD_AUTO_MODERATION_RULE.compile(guild=123321) + rest_client._request = mock.AsyncMock(return_value={"id": "494949494"}) + + result = await rest_client.create_auto_mod_rule( + StubModel(123321), + name="meow", + event_type=auto_mod.AutoModEventType.MESSAGE_SEND, + trigger_type=auto_mod.AutoModTriggerType.HARMFUL_LINK, + actions=[mock_action], + allow_list=["bye", "meeeeeeow"], + keyword_filter=["okokok", "no", "bye"], + presets=[auto_mod.AutoModKeywordPresetType.PROFANITY, auto_mod.AutoModKeywordPresetType.SEXUAL_CONTENT], + enabled=False, + exempt_roles=[StubModel(4212), StubModel(43123)], + exempt_channels=[StubModel(566), StubModel(333), StubModel(222)], + reason="a reason meow", + ) + + assert result is rest_client._entity_factory.deserialize_auto_mod_rule.return_value + rest_client._entity_factory.deserialize_auto_mod_rule.assert_called_once_with(rest_client._request.return_value) + rest_client._request.assert_awaited_once_with( + expected_route, + json={ + "name": "meow", + "event_type": 1, + "trigger_type": 2, + "trigger_metadata": { + "keyword_filter": ["okokok", "no", "bye"], + "presets": [1, 2], + "allow_list": ["bye", "meeeeeeow"], + }, + "actions": [rest_client._entity_factory.serialize_auto_mod_action.return_value], + "enabled": False, + "exempt_roles": ["4212", "43123"], + "exempt_channels": ["566", "333", "222"], + }, + reason="a reason meow", + ) + rest_client._entity_factory.serialize_auto_mod_action.assert_called_once_with(mock_action) + + async def test_create_auto_mod_rule_partial(self, rest_client: rest.RESTClientImpl): + mock_action = {"type": "58585858"} + expected_route = routes.POST_GUILD_AUTO_MODERATION_RULE.compile(guild=123321) + rest_client._request = mock.AsyncMock(return_value={"id": "494949494"}) + + result = await rest_client.create_auto_mod_rule( + StubModel(123321), + name="meow", + event_type=auto_mod.AutoModEventType.MESSAGE_SEND, + trigger_type=auto_mod.AutoModTriggerType.HARMFUL_LINK, + actions=[mock_action], + ) + + assert result is rest_client._entity_factory.deserialize_auto_mod_rule.return_value + rest_client._entity_factory.deserialize_auto_mod_rule.assert_called_once_with(rest_client._request.return_value) + rest_client._request.assert_awaited_once_with( + expected_route, + json={ + "name": "meow", + "event_type": 1, + "trigger_type": 2, + "actions": [rest_client._entity_factory.serialize_auto_mod_action.return_value], + "enabled": True, + }, + reason=undefined.UNDEFINED, + ) + rest_client._entity_factory.serialize_auto_mod_action.assert_called_once_with(mock_action) + + async def test_edit_auto_mod_rule(self, rest_client: rest.RESTClientImpl): + mock_action = {"type": "58585858"} + expected_route = routes.PATCH_GUILD_AUTO_MODERATION_RULE.compile(guild=123321, rule=5412123) + rest_client._request = mock.AsyncMock(return_value={"id": "494949494"}) + + result = await rest_client.edit_auto_mod_rule( + StubModel(123321), + StubModel(5412123), + name="meow", + event_type=auto_mod.AutoModEventType.MESSAGE_SEND, + actions=[mock_action], + allow_list=["bye", "beep"], + keyword_filter=["sdsdsd", "eee", "bye"], + presets=[auto_mod.AutoModKeywordPresetType.SEXUAL_CONTENT, auto_mod.AutoModKeywordPresetType.PROFANITY], + enabled=False, + exempt_roles=[StubModel(4545), StubModel(5656)], + exempt_channels=[StubModel(555), StubModel(666), StubModel(777)], + reason="nyaa nyaa", + ) + + assert result is rest_client._entity_factory.deserialize_auto_mod_rule.return_value + rest_client._entity_factory.deserialize_auto_mod_rule.assert_called_once_with(rest_client._request.return_value) + rest_client._request.assert_awaited_once_with( + expected_route, + json={ + "name": "meow", + "event_type": 1, + "trigger_metadata": { + "keyword_filter": ["sdsdsd", "eee", "bye"], + "presets": [2, 1], + "allow_list": ["bye", "beep"], + }, + "actions": [rest_client._entity_factory.serialize_auto_mod_action.return_value], + "enabled": False, + "exempt_roles": ["4545", "5656"], + "exempt_channels": ["555", "666", "777"], + }, + reason="nyaa nyaa", + ) + rest_client._entity_factory.serialize_auto_mod_action.assert_called_once_with(mock_action) + + async def test_edit_auto_mod_rule_partial(self, rest_client: rest.RESTClientImpl): + expected_route = routes.PATCH_GUILD_AUTO_MODERATION_RULE.compile(guild=123321, rule=44332222) + rest_client._request = mock.AsyncMock(return_value={"id": "494949494"}) + + result = await rest_client.edit_auto_mod_rule( + StubModel(123321), + StubModel(44332222), + ) + + assert result is rest_client._entity_factory.deserialize_auto_mod_rule.return_value + rest_client._entity_factory.deserialize_auto_mod_rule.assert_called_once_with(rest_client._request.return_value) + rest_client._request.assert_awaited_once_with(expected_route, json={}, reason=undefined.UNDEFINED) + rest_client._entity_factory.serialize_auto_mod_action.assert_not_called() + + async def test_delete_auto_mod_rule(self, rest_client: rest.RESTClientImpl): + expected_route = routes.DELETE_GUILD_AUTO_MODERATION_RULE.compile(guild=54123, rule=651234) + rest_client._request = mock.AsyncMock() + + result = await rest_client.delete_auto_mod_rule(StubModel(54123), StubModel(651234), reason="ok hi") + + assert result is None + rest_client._request.assert_awaited_once_with(expected_route, reason="ok hi") diff --git a/tests/hikari/test_audit_logs.py b/tests/hikari/test_audit_logs.py index 2b3eac586b..0b10caa780 100644 --- a/tests/hikari/test_audit_logs.py +++ b/tests/hikari/test_audit_logs.py @@ -114,6 +114,7 @@ def test_iter(self): entry_2 = object() entry_3 = object() audit_log = audit_logs.AuditLog( + auto_mod_rules={}, entries={ snowflakes.Snowflake(432123): entry_1, snowflakes.Snowflake(432654): entry_2, @@ -130,6 +131,7 @@ def test_get_item_with_index(self): entry = object() entry_2 = object() audit_log = audit_logs.AuditLog( + auto_mod_rules={}, entries={ snowflakes.Snowflake(432123): object(), snowflakes.Snowflake(432654): entry, @@ -149,6 +151,7 @@ def test_get_item_with_slice(self): entry_1 = object() entry_2 = object() audit_log = audit_logs.AuditLog( + auto_mod_rules={}, entries={ snowflakes.Snowflake(432123): object(), snowflakes.Snowflake(432654): entry_1, @@ -165,6 +168,7 @@ def test_get_item_with_slice(self): def test_len(self): audit_log = audit_logs.AuditLog( + auto_mod_rules={}, entries={ snowflakes.Snowflake(432123): object(), snowflakes.Snowflake(432654): object(),