Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/1230.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add support for guild incident actions.
- Add :class:`IncidentsData` and :attr:`Guild.incidents_data` attribute.
- New ``invites_disabled_until`` and ``dms_disabled_until`` parameters for :meth:`Guild.edit`.
152 changes: 151 additions & 1 deletion disnake/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
from .widget import Widget, WidgetSettings

__all__ = (
"IncidentsData",
"Guild",
"GuildBuilder",
)
Expand All @@ -110,6 +111,7 @@
CreateGuildPlaceholderRole,
Guild as GuildPayload,
GuildFeature,
IncidentsData as IncidentsDataPayload,
MFALevel,
)
from .types.integration import Integration as IntegrationPayload, IntegrationType
Expand All @@ -135,6 +137,94 @@ class _GuildLimit(NamedTuple):
sounds: int


class IncidentsData:
"""Represents data about various security incidents/actions in a guild.

.. collapse:: operations

.. describe:: x == y

Checks if two ``IncidentsData`` instances are equal.

.. describe:: x != y

Checks if two ``IncidentsData`` instances are not equal.

.. versionadded:: 2.10

Attributes
----------
dm_spam_detected_at: Optional[:class:`datetime.datetime`]
The time (in UTC) at which DM spam was last detected.
raid_detected_at: Optional[:class:`datetime.datetime`]
The time (in UTC) at which a raid was last detected.
"""

__slots__ = (
"_invites_disabled_until",
"_dms_disabled_until",
"dm_spam_detected_at",
"raid_detected_at",
)

def __init__(self, data: IncidentsDataPayload) -> None:
self._invites_disabled_until: Optional[datetime.datetime] = utils.parse_time(
data.get("invites_disabled_until")
)
self._dms_disabled_until: Optional[datetime.datetime] = utils.parse_time(
data.get("dms_disabled_until")
)
self.dm_spam_detected_at: Optional[datetime.datetime] = utils.parse_time(
data.get("dm_spam_detected_at")
)
self.raid_detected_at: Optional[datetime.datetime] = utils.parse_time(
data.get("raid_detected_at")
)

@property
def invites_disabled_until(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: Returns the time (in UTC) until
which users cannot join the server via invites, if any.
"""
if (
self._invites_disabled_until is not None
and self._invites_disabled_until < utils.utcnow()
):
self._invites_disabled_until = None

return self._invites_disabled_until

@property
def dms_disabled_until(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: Returns the time (in UTC) until
which members cannot send DMs to each other, if any.

This does not apply to moderators, bots, or members who are
already friends with each other.
"""
if self._dms_disabled_until is not None and self._dms_disabled_until < utils.utcnow():
self._dms_disabled_until = None

return self._dms_disabled_until

def __eq__(self, other: Any) -> bool:
return (
isinstance(other, IncidentsData)
and self.invites_disabled_until == other.invites_disabled_until
and self.dms_disabled_until == other.dms_disabled_until
and self.dm_spam_detected_at == other.dm_spam_detected_at
and self.raid_detected_at == other.raid_detected_at
)

def __repr__(self) -> str:
return (
f"<IncidentsData invites_disabled_until={self.invites_disabled_until!r}"
f" dms_disabled_until={self.dms_disabled_until!r}"
f" dm_spam_detected_at={self.dm_spam_detected_at!r}"
f" raid_detected_at={self.raid_detected_at!r}>"
)


class Guild(Hashable):
"""Represents a Discord guild.

Expand Down Expand Up @@ -322,6 +412,11 @@ class Guild(Hashable):
To get a full :class:`Invite` object, see :attr:`Guild.vanity_invite`.

.. versionadded:: 2.5

incidents_data: Optional[:class:`IncidentsData`]
Data about various security incidents/actions in this guild, like disabled invites/DMs.

.. versionadded:: 2.10
"""

__slots__ = (
Expand Down Expand Up @@ -354,6 +449,7 @@ class Guild(Hashable):
"widget_enabled",
"widget_channel_id",
"vanity_url_code",
"incidents_data",
"_members",
"_channels",
"_icon",
Expand Down Expand Up @@ -600,6 +696,11 @@ def _from_data(self, guild: GuildPayload) -> None:
self._safety_alerts_channel_id: Optional[int] = utils._get_as_snowflake(
guild, "safety_alerts_channel_id"
)
self.incidents_data: Optional[IncidentsData] = (
IncidentsData(incidents_data)
if (incidents_data := guild.get("incidents_data"))
else None
)

stage_instances = guild.get("stage_instances")
if stage_instances is not None:
Expand Down Expand Up @@ -2040,6 +2141,8 @@ async def edit(
discovery_splash: Optional[AssetBytes] = MISSING,
community: bool = MISSING,
invites_disabled: bool = MISSING,
invites_disabled_until: Optional[Union[datetime.datetime, datetime.timedelta]] = MISSING,
dms_disabled_until: Optional[Union[datetime.datetime, datetime.timedelta]] = MISSING,
raid_alerts_disabled: bool = MISSING,
afk_channel: Optional[VoiceChannel] = MISSING,
owner: Snowflake = MISSING,
Expand Down Expand Up @@ -2125,7 +2228,8 @@ async def edit(
Whether the guild should be a Community guild. If set to ``True``\\, both ``rules_channel``
and ``public_updates_channel`` parameters are required.
invites_disabled: :class:`bool`
Whether the guild has paused invites, preventing new users from joining.
Whether the guild has paused invites (indefinitely), preventing new users from joining.
See also the ``invites_disabled_until`` parameter.

This is only available to guilds that contain ``COMMUNITY``
in :attr:`Guild.features`.
Expand All @@ -2134,6 +2238,28 @@ async def edit(

.. versionadded:: 2.6

invites_disabled_until: Optional[Union[:class:`datetime.datetime`, :class:`datetime.timedelta`]]
The time until/for which invites are paused, up to 24 hours in the future.
See also the ``invites_disabled`` parameter.
Can be set to ``None`` to re-enable invites.

This is only available to guilds that contain ``COMMUNITY``
in :attr:`Guild.features`.

.. versionadded:: 2.10

dms_disabled_until: Union[:class:`datetime.datetime`, :class:`datetime.timedelta`]
The time until/for which DMs between guild members are disabled, up to 24 hours in the future.
Can be set to ``None`` to re-enable DMs.

This does not apply to moderators, bots, or members who are
already friends with each other.

This is only available to guilds that contain ``COMMUNITY``
in :attr:`Guild.features`.

.. versionadded:: 2.10

raid_alerts_disabled: :class:`bool`
Whether the guild has disabled join raid alerts.

Expand Down Expand Up @@ -2220,6 +2346,30 @@ async def edit(
if vanity_code is not MISSING:
await http.change_vanity_code(self.id, vanity_code, reason=reason)

if invites_disabled_until is not MISSING or dms_disabled_until is not MISSING:
payload: IncidentsDataPayload = {}

# we need to include the old values, otherwise Discord will consider them set to `null`
# (which would e.g. re-enable DMs when disabling invites)
if self.incidents_data:
if invites_disabled_until is MISSING:
invites_disabled_until = self.incidents_data.invites_disabled_until
if dms_disabled_until is MISSING:
dms_disabled_until = self.incidents_data.dms_disabled_until

if invites_disabled_until is not MISSING:
if isinstance(invites_disabled_until, datetime.timedelta):
invites_disabled_until = utils.utcnow() + invites_disabled_until
payload["invites_disabled_until"] = utils.isoformat_utc(invites_disabled_until)

if dms_disabled_until is not MISSING:
if isinstance(dms_disabled_until, datetime.timedelta):
dms_disabled_until = utils.utcnow() + dms_disabled_until
payload["dms_disabled_until"] = utils.isoformat_utc(dms_disabled_until)

if payload:
await http.edit_guild_incident_actions(self.id, payload)

fields: Dict[str, Any] = {}
if name is not MISSING:
fields["name"] = name
Expand Down
6 changes: 6 additions & 0 deletions disnake/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,12 @@ def edit_guild(
Route("PATCH", "/guilds/{guild_id}", guild_id=guild_id), json=payload, reason=reason
)

def edit_guild_incident_actions(
self, guild_id: Snowflake, payload: guild.IncidentsData
) -> Response[guild.IncidentsData]:
r = Route("PUT", "/guilds/{guild_id}/incident-actions", guild_id=guild_id)
return self.request(r, json=payload)

def get_template(self, code: str) -> Response[template.Template]:
return self.request(Route("GET", "/guilds/templates/{code}", code=code))

Expand Down
9 changes: 4 additions & 5 deletions disnake/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,12 +756,11 @@ def current_timeout(self) -> Optional[datetime.datetime]:

.. versionadded:: 2.3
"""
if self._communication_disabled_until is None:
return None

if self._communication_disabled_until < utils.utcnow():
if (
self._communication_disabled_until is not None
and self._communication_disabled_until < utils.utcnow()
):
self._communication_disabled_until = None
return None

return self._communication_disabled_until

Expand Down
8 changes: 8 additions & 0 deletions disnake/types/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ class UnavailableGuild(TypedDict):
]


class IncidentsData(TypedDict, total=False):
invites_disabled_until: Optional[str]
dms_disabled_until: Optional[str]
dm_spam_detected_at: Optional[str]
raid_detected_at: Optional[str]


class _BaseGuildPreview(UnavailableGuild):
name: str
icon: Optional[str]
Expand Down Expand Up @@ -138,6 +145,7 @@ class Guild(_BaseGuildPreview):
stickers: NotRequired[List[GuildSticker]]
premium_progress_bar_enabled: bool
safety_alerts_channel_id: Optional[Snowflake]
incidents_data: Optional[IncidentsData]

# specific to GUILD_CREATE event
joined_at: NotRequired[Optional[str]]
Expand Down
8 changes: 8 additions & 0 deletions docs/api/guilds.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ OnboardingPromptOption
.. autoclass:: OnboardingPromptOption()
:members:

IncidentsData
~~~~~~~~~~~~~

.. attributetable:: IncidentsData

.. autoclass:: IncidentsData()
:members:

Data Classes
------------

Expand Down
Loading