Skip to content

Commit bd82acd

Browse files
feat(guild): support guild incidents (#1230)
Co-authored-by: arielle <[email protected]>
1 parent f3a9b0b commit bd82acd

File tree

6 files changed

+180
-6
lines changed

6 files changed

+180
-6
lines changed

changelog/1230.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add support for guild incident actions.
2+
- Add :class:`IncidentsData` and :attr:`Guild.incidents_data` attribute.
3+
- New ``invites_disabled_until`` and ``dms_disabled_until`` parameters for :meth:`Guild.edit`.

disnake/guild.py

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
from .widget import Widget, WidgetSettings
8585

8686
__all__ = (
87+
"IncidentsData",
8788
"Guild",
8889
"GuildBuilder",
8990
)
@@ -110,6 +111,7 @@
110111
CreateGuildPlaceholderRole,
111112
Guild as GuildPayload,
112113
GuildFeature,
114+
IncidentsData as IncidentsDataPayload,
113115
MFALevel,
114116
)
115117
from .types.integration import Integration as IntegrationPayload, IntegrationType
@@ -135,6 +137,94 @@ class _GuildLimit(NamedTuple):
135137
sounds: int
136138

137139

140+
class IncidentsData:
141+
"""Represents data about various security incidents/actions in a guild.
142+
143+
.. collapse:: operations
144+
145+
.. describe:: x == y
146+
147+
Checks if two ``IncidentsData`` instances are equal.
148+
149+
.. describe:: x != y
150+
151+
Checks if two ``IncidentsData`` instances are not equal.
152+
153+
.. versionadded:: 2.11
154+
155+
Attributes
156+
----------
157+
dm_spam_detected_at: Optional[:class:`datetime.datetime`]
158+
The time (in UTC) at which DM spam was last detected.
159+
raid_detected_at: Optional[:class:`datetime.datetime`]
160+
The time (in UTC) at which a raid was last detected.
161+
"""
162+
163+
__slots__ = (
164+
"_invites_disabled_until",
165+
"_dms_disabled_until",
166+
"dm_spam_detected_at",
167+
"raid_detected_at",
168+
)
169+
170+
def __init__(self, data: IncidentsDataPayload) -> None:
171+
self._invites_disabled_until: Optional[datetime.datetime] = utils.parse_time(
172+
data.get("invites_disabled_until")
173+
)
174+
self._dms_disabled_until: Optional[datetime.datetime] = utils.parse_time(
175+
data.get("dms_disabled_until")
176+
)
177+
self.dm_spam_detected_at: Optional[datetime.datetime] = utils.parse_time(
178+
data.get("dm_spam_detected_at")
179+
)
180+
self.raid_detected_at: Optional[datetime.datetime] = utils.parse_time(
181+
data.get("raid_detected_at")
182+
)
183+
184+
@property
185+
def invites_disabled_until(self) -> Optional[datetime.datetime]:
186+
"""Optional[:class:`datetime.datetime`]: Returns the time (in UTC) until
187+
which users cannot join the server via invites, if any.
188+
"""
189+
if (
190+
self._invites_disabled_until is not None
191+
and self._invites_disabled_until < utils.utcnow()
192+
):
193+
self._invites_disabled_until = None
194+
195+
return self._invites_disabled_until
196+
197+
@property
198+
def dms_disabled_until(self) -> Optional[datetime.datetime]:
199+
"""Optional[:class:`datetime.datetime`]: Returns the time (in UTC) until
200+
which members cannot send DMs to each other, if any.
201+
202+
This does not apply to moderators, bots, or members who are
203+
already friends with each other.
204+
"""
205+
if self._dms_disabled_until is not None and self._dms_disabled_until < utils.utcnow():
206+
self._dms_disabled_until = None
207+
208+
return self._dms_disabled_until
209+
210+
def __eq__(self, other: Any) -> bool:
211+
return (
212+
isinstance(other, IncidentsData)
213+
and self.invites_disabled_until == other.invites_disabled_until
214+
and self.dms_disabled_until == other.dms_disabled_until
215+
and self.dm_spam_detected_at == other.dm_spam_detected_at
216+
and self.raid_detected_at == other.raid_detected_at
217+
)
218+
219+
def __repr__(self) -> str:
220+
return (
221+
f"<IncidentsData invites_disabled_until={self.invites_disabled_until!r}"
222+
f" dms_disabled_until={self.dms_disabled_until!r}"
223+
f" dm_spam_detected_at={self.dm_spam_detected_at!r}"
224+
f" raid_detected_at={self.raid_detected_at!r}>"
225+
)
226+
227+
138228
class Guild(Hashable):
139229
"""Represents a Discord guild.
140230
@@ -322,6 +412,11 @@ class Guild(Hashable):
322412
To get a full :class:`Invite` object, see :attr:`Guild.vanity_invite`.
323413
324414
.. versionadded:: 2.5
415+
416+
incidents_data: Optional[:class:`IncidentsData`]
417+
Data about various security incidents/actions in this guild, like disabled invites/DMs.
418+
419+
.. versionadded:: 2.11
325420
"""
326421

327422
__slots__ = (
@@ -354,6 +449,7 @@ class Guild(Hashable):
354449
"widget_enabled",
355450
"widget_channel_id",
356451
"vanity_url_code",
452+
"incidents_data",
357453
"_members",
358454
"_channels",
359455
"_icon",
@@ -600,6 +696,11 @@ def _from_data(self, guild: GuildPayload) -> None:
600696
self._safety_alerts_channel_id: Optional[int] = utils._get_as_snowflake(
601697
guild, "safety_alerts_channel_id"
602698
)
699+
self.incidents_data: Optional[IncidentsData] = (
700+
IncidentsData(incidents_data)
701+
if (incidents_data := guild.get("incidents_data"))
702+
else None
703+
)
603704

604705
stage_instances = guild.get("stage_instances")
605706
if stage_instances is not None:
@@ -2040,6 +2141,8 @@ async def edit(
20402141
discovery_splash: Optional[AssetBytes] = MISSING,
20412142
community: bool = MISSING,
20422143
invites_disabled: bool = MISSING,
2144+
invites_disabled_until: Optional[Union[datetime.datetime, datetime.timedelta]] = MISSING,
2145+
dms_disabled_until: Optional[Union[datetime.datetime, datetime.timedelta]] = MISSING,
20432146
raid_alerts_disabled: bool = MISSING,
20442147
afk_channel: Optional[VoiceChannel] = MISSING,
20452148
owner: Snowflake = MISSING,
@@ -2125,7 +2228,8 @@ async def edit(
21252228
Whether the guild should be a Community guild. If set to ``True``\\, both ``rules_channel``
21262229
and ``public_updates_channel`` parameters are required.
21272230
invites_disabled: :class:`bool`
2128-
Whether the guild has paused invites, preventing new users from joining.
2231+
Whether the guild has paused invites (indefinitely), preventing new users from joining.
2232+
See also the ``invites_disabled_until`` parameter.
21292233
21302234
This is only available to guilds that contain ``COMMUNITY``
21312235
in :attr:`Guild.features`.
@@ -2134,6 +2238,28 @@ async def edit(
21342238
21352239
.. versionadded:: 2.6
21362240
2241+
invites_disabled_until: Optional[Union[:class:`datetime.datetime`, :class:`datetime.timedelta`]]
2242+
The time until/for which invites are paused, up to 24 hours in the future.
2243+
See also the ``invites_disabled`` parameter.
2244+
Can be set to ``None`` to re-enable invites.
2245+
2246+
This is only available to guilds that contain ``COMMUNITY``
2247+
in :attr:`Guild.features`.
2248+
2249+
.. versionadded:: 2.11
2250+
2251+
dms_disabled_until: Union[:class:`datetime.datetime`, :class:`datetime.timedelta`]
2252+
The time until/for which DMs between guild members are disabled, up to 24 hours in the future.
2253+
Can be set to ``None`` to re-enable DMs.
2254+
2255+
This does not apply to moderators, bots, or members who are
2256+
already friends with each other.
2257+
2258+
This is only available to guilds that contain ``COMMUNITY``
2259+
in :attr:`Guild.features`.
2260+
2261+
.. versionadded:: 2.11
2262+
21372263
raid_alerts_disabled: :class:`bool`
21382264
Whether the guild has disabled join raid alerts.
21392265
@@ -2220,6 +2346,30 @@ async def edit(
22202346
if vanity_code is not MISSING:
22212347
await http.change_vanity_code(self.id, vanity_code, reason=reason)
22222348

2349+
if invites_disabled_until is not MISSING or dms_disabled_until is not MISSING:
2350+
payload: IncidentsDataPayload = {}
2351+
2352+
# we need to include the old values, otherwise Discord will consider them set to `null`
2353+
# (which would e.g. re-enable DMs when disabling invites)
2354+
if self.incidents_data:
2355+
if invites_disabled_until is MISSING:
2356+
invites_disabled_until = self.incidents_data.invites_disabled_until
2357+
if dms_disabled_until is MISSING:
2358+
dms_disabled_until = self.incidents_data.dms_disabled_until
2359+
2360+
if invites_disabled_until is not MISSING:
2361+
if isinstance(invites_disabled_until, datetime.timedelta):
2362+
invites_disabled_until = utils.utcnow() + invites_disabled_until
2363+
payload["invites_disabled_until"] = utils.isoformat_utc(invites_disabled_until)
2364+
2365+
if dms_disabled_until is not MISSING:
2366+
if isinstance(dms_disabled_until, datetime.timedelta):
2367+
dms_disabled_until = utils.utcnow() + dms_disabled_until
2368+
payload["dms_disabled_until"] = utils.isoformat_utc(dms_disabled_until)
2369+
2370+
if payload:
2371+
await http.edit_guild_incident_actions(self.id, payload)
2372+
22232373
fields: Dict[str, Any] = {}
22242374
if name is not MISSING:
22252375
fields["name"] = name

disnake/http.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,12 @@ def edit_guild(
14821482
Route("PATCH", "/guilds/{guild_id}", guild_id=guild_id), json=payload, reason=reason
14831483
)
14841484

1485+
def edit_guild_incident_actions(
1486+
self, guild_id: Snowflake, payload: guild.IncidentsData
1487+
) -> Response[guild.IncidentsData]:
1488+
r = Route("PUT", "/guilds/{guild_id}/incident-actions", guild_id=guild_id)
1489+
return self.request(r, json=payload)
1490+
14851491
def get_template(self, code: str) -> Response[template.Template]:
14861492
return self.request(Route("GET", "/guilds/templates/{code}", code=code))
14871493

disnake/member.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -756,12 +756,11 @@ def current_timeout(self) -> Optional[datetime.datetime]:
756756
757757
.. versionadded:: 2.3
758758
"""
759-
if self._communication_disabled_until is None:
760-
return None
761-
762-
if self._communication_disabled_until < utils.utcnow():
759+
if (
760+
self._communication_disabled_until is not None
761+
and self._communication_disabled_until < utils.utcnow()
762+
):
763763
self._communication_disabled_until = None
764-
return None
765764

766765
return self._communication_disabled_until
767766

disnake/types/guild.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ class UnavailableGuild(TypedDict):
8787
]
8888

8989

90+
class IncidentsData(TypedDict, total=False):
91+
invites_disabled_until: Optional[str]
92+
dms_disabled_until: Optional[str]
93+
dm_spam_detected_at: Optional[str]
94+
raid_detected_at: Optional[str]
95+
96+
9097
class _BaseGuildPreview(UnavailableGuild):
9198
name: str
9299
icon: Optional[str]
@@ -138,6 +145,7 @@ class Guild(_BaseGuildPreview):
138145
stickers: NotRequired[List[GuildSticker]]
139146
premium_progress_bar_enabled: bool
140147
safety_alerts_channel_id: Optional[Snowflake]
148+
incidents_data: Optional[IncidentsData]
141149

142150
# specific to GUILD_CREATE event
143151
joined_at: NotRequired[Optional[str]]

docs/api/guilds.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ OnboardingPromptOption
113113
.. autoclass:: OnboardingPromptOption()
114114
:members:
115115

116+
IncidentsData
117+
~~~~~~~~~~~~~
118+
119+
.. attributetable:: IncidentsData
120+
121+
.. autoclass:: IncidentsData()
122+
:members:
123+
116124
Data Classes
117125
------------
118126

0 commit comments

Comments
 (0)