Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(guild): support guild incidents #1230

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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`.
146 changes: 145 additions & 1 deletion disnake/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
from .widget import Widget, WidgetSettings

__all__ = (
"IncidentsData",
"Guild",
"GuildBuilder",
)
Expand All @@ -106,6 +107,7 @@
CreateGuildPlaceholderRole,
Guild as GuildPayload,
GuildFeature,
IncidentsData as IncidentsDataPayload,
MFALevel,
)
from .types.integration import IntegrationType
Expand All @@ -130,6 +132,86 @@ class _GuildLimit(NamedTuple):
filesize: 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
)


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

Expand Down Expand Up @@ -309,6 +391,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 @@ -340,6 +427,7 @@ class Guild(Hashable):
"widget_enabled",
"widget_channel_id",
"vanity_url_code",
"incidents_data",
"_members",
"_channels",
"_icon",
Expand Down Expand Up @@ -583,6 +671,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 @@ -2012,6 +2105,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 @@ -2097,7 +2192,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 @@ -2106,6 +2202,30 @@ 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.
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.

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

Can be set to ``None`` to re-enable DMs.

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 @@ -2192,6 +2312,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 @@ -1397,6 +1397,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 @@ -721,12 +721,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 @@ -84,6 +84,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 @@ -135,6 +142,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