diff --git a/discord/__init__.py b/discord/__init__.py index dbd87452b2..1524dc4c3f 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -47,6 +47,7 @@ from .flags import * from .guild import * from .http import * +from .incidents import * from .integrations import * from .interactions import * from .invite import * diff --git a/discord/guild.py b/discord/guild.py index ff4c9e04b2..087c1349e3 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -26,6 +26,7 @@ from __future__ import annotations import copy +import datetime import unicodedata from typing import ( TYPE_CHECKING, @@ -68,6 +69,7 @@ from .errors import ClientException, InvalidArgument, InvalidData from .file import File from .flags import SystemChannelFlags +from .incidents import IncidentsData from .integrations import Integration, _integration_factory from .invite import Invite from .iterators import ( @@ -112,7 +114,11 @@ from .template import Template from .types.guild import Ban as BanPayload from .types.guild import Guild as GuildPayload - from .types.guild import GuildFeature, MFALevel + from .types.guild import ( + GuildFeature, + MFALevel, + ) + from .types.guild import ModifyIncidents as ModifyIncidentsPayload from .types.member import Member as MemberPayload from .types.threads import Thread as ThreadPayload from .types.voice import GuildVoiceState @@ -290,6 +296,7 @@ class Guild(Hashable): "approximate_member_count", "approximate_presence_count", "_sounds", + "incidents_data", ) _PREMIUM_GUILD_LIMITS: ClassVar[dict[int | None, _GuildLimit]] = { @@ -569,6 +576,13 @@ def _from_data(self, guild: GuildPayload) -> None: sound = SoundboardSound(state=state, http=state.http, data=sound) self._add_sound(sound) + incidents_payload = guild.get("incidents_data") + self.incidents_data: IncidentsData | None = ( + IncidentsData(data=incidents_payload) + if incidents_payload is not None + else None + ) + def _add_sound(self, sound: SoundboardSound) -> None: self._sounds[sound.id] = sound self._state._add_sound(sound) @@ -4405,6 +4419,52 @@ async def edit_onboarding( new = await self._state.http.edit_onboarding(self.id, fields, reason=reason) return Onboarding(data=new, guild=self) + async def modify_incident_actions( + self, + *, + invites_disabled_until: datetime.datetime | None = MISSING, + dms_disabled_until: datetime.datetime | None = MISSING, + reason: str | None = MISSING, + ) -> IncidentsData: + """|coro| + + Modify the guild's incident actions, controlling when invites or DMs + are re-enabled after being temporarily disabled. Requires + the :attr:`~Permissions.manage_guild` permission. + + Parameters + ---------- + invites_disabled_until: Optional[:class:`datetime.datetime`] + The ISO8601 timestamp indicating when invites will be enabled again, + or ``None`` to enable invites immediately. + dms_disabled_until: Optional[:class:`datetime.datetime`] + The ISO8601 timestamp indicating when DMs will be enabled again, + or ``None`` to enable DMs immediately. + reason: Optional[:class:`str`] + The reason for this action, used for the audit log. + + Returns + ------- + :class:`IncidentsData` + The updated incidents data for the guild. + """ + + fields: ModifyIncidentsPayload = {} + if invites_disabled_until is not MISSING: + fields["invites_disabled_until"] = ( + invites_disabled_until and invites_disabled_until.isoformat() + ) + + if dms_disabled_until is not MISSING: + fields["dms_disabled_until"] = ( + dms_disabled_until and dms_disabled_until.isoformat() + ) + + new = await self._state.http.modify_guild_incident_actions( + self.id, fields, reason=reason + ) + return IncidentsData(data=new) + async def delete_auto_moderation_rule( self, id: int, diff --git a/discord/http.py b/discord/http.py index bfefed91d1..47ecfb0325 100644 --- a/discord/http.py +++ b/discord/http.py @@ -47,7 +47,7 @@ from .file import VoiceMessage from .gateway import DiscordClientWebSocketResponse from .soundboard import PartialSoundboardSound, SoundboardSound -from .utils import MISSING, warn_deprecated +from .utils import MISSING _log = logging.getLogger(__name__) @@ -3135,6 +3135,19 @@ def edit_onboarding( reason=reason, ) + def modify_guild_incident_actions( + self, + guild_id: Snowflake, + payload: guild.ModifyIncidents, + *, + reason: str | None = None, + ) -> Response[guild.IncidentsData]: + return self.request( + Route("PUT", "/guilds/{guild_id}/incident-actions", guild_id=guild_id), + json=payload, + reason=reason, + ) + # Polls def expire_poll( diff --git a/discord/incidents.py b/discord/incidents.py new file mode 100644 index 0000000000..e3f45d2b28 --- /dev/null +++ b/discord/incidents.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING + +from .utils import parse_time + +if TYPE_CHECKING: + from .types.guild import IncidentsData as IncidentsDataPayload + +__all__ = ("IncidentsData",) + + +class IncidentsData: + """Represents the incidents data object for a guild. + + Attributes + ---------- + invites_disabled_until: Optional[datetime.datetime] + When invites will be enabled again as a :class:`datetime.datetime`, or ``None``. + dms_disabled_until: Optional[datetime.datetime] + When direct messages will be enabled again as a :class:`datetime.datetime`, or ``None``. + dm_spam_detected_at: Optional[datetime.datetime] + When DM spam was detected, or ``None``. + raid_detected_at: Optional[datetime.datetime] + When a raid was detected, or ``None``. + """ + + __slots__ = ( + "invites_disabled_until", + "dms_disabled_until", + "dm_spam_detected_at", + "raid_detected_at", + ) + + def __init__(self, data: IncidentsDataPayload): + self.invites_disabled_until: datetime.datetime | None = parse_time( + data.get("invites_disabled_until") + ) + + self.dms_disabled_until: datetime.datetime | None = parse_time( + data.get("dms_disabled_until") + ) + + self.dm_spam_detected_at: datetime.datetime | None = parse_time( + data.get("dm_spam_detected_at") + ) + + self.raid_detected_at: datetime.datetime | None = parse_time( + data.get("raid_detected_at") + ) + + def to_dict(self) -> IncidentsDataPayload: + return { + "invites_disabled_until": self.invites_disabled_until + and self.invites_disabled_until.isoformat(), + "dms_disabled_until": self.dms_disabled_until + and self.dms_disabled_until.isoformat(), + "dm_spam_detected_at": self.dm_spam_detected_at + and self.dm_spam_detected_at.isoformat(), + "raid_detected_at": self.raid_detected_at + and self.raid_detected_at.isoformat(), + } diff --git a/discord/types/guild.py b/discord/types/guild.py index 342686af9e..3c71c8647e 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -164,6 +164,7 @@ class Guild(_BaseGuildPreview): premium_tier: PremiumTier preferred_locale: str public_updates_channel_id: Snowflake | None + incidents_data: IncidentsData | None class InviteGuild(Guild, total=False): @@ -197,3 +198,15 @@ class GuildMFAModify(TypedDict): class GuildBulkBan(TypedDict): banned_users: list[Snowflake] failed_users: list[Snowflake] + + +class IncidentsData(TypedDict, total=False): + invites_disabled_until: str | None + dms_disabled_until: str | None + dm_spam_detected_at: str | None + raid_detected_at: str | None + + +class ModifyIncidents(TypedDict, total=False): + invites_disabled_until: str | None + dms_disabled_until: str | None