Skip to content

Commit

Permalink
Merge branch 'master' into feature/premium-apps-onetime-purchases
Browse files Browse the repository at this point in the history
  • Loading branch information
shiftinv committed Jun 12, 2024
2 parents a5c82de + 06281ba commit fa81c0f
Show file tree
Hide file tree
Showing 21 changed files with 346 additions and 33 deletions.
1 change: 1 addition & 0 deletions changelog/1174.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support banning multiple users at once using :meth:`Guild.bulk_ban`.
1 change: 1 addition & 0 deletions changelog/1189.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix base URL for stickers with :attr:`StickerFormatType.gif`.
5 changes: 5 additions & 0 deletions changelog/1197.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Implement new :attr:`Message.type`\s:
- :attr:`MessageType.guild_incident_alert_mode_enabled`
- :attr:`MessageType.guild_incident_alert_mode_disabled`
- :attr:`MessageType.guild_incident_report_raid`
- :attr:`MessageType.guild_incident_report_false_alarm`
4 changes: 4 additions & 0 deletions changelog/889.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add support for avatar decorations using:
- :attr:`User.avatar_decoration`
- :attr:`Member.display_avatar_decoration`
- :attr:`Member.guild_avatar_decoration`
13 changes: 13 additions & 0 deletions disnake/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ class Asset(AssetMixin):

BASE = "https://cdn.discordapp.com"

# only used in special cases where Discord doesn't provide an asset on the CDN url
BASE_MEDIA = "https://media.discordapp.net"

def __init__(self, state: AnyState, *, url: str, key: str, animated: bool = False) -> None:
self._state: AnyState = state
self._url: str = url
Expand Down Expand Up @@ -314,6 +317,16 @@ def _from_guild_scheduled_event_image(
animated=False,
)

@classmethod
def _from_avatar_decoration(cls, state: AnyState, avatar_decoration_asset: str) -> Self:
animated = avatar_decoration_asset.startswith("a_")
return cls(
state,
url=f"{cls.BASE}/avatar-decoration-presets/{avatar_decoration_asset}.png?size=1024",
key=avatar_decoration_asset,
animated=animated,
)

def __str__(self) -> str:
return self._url

Expand Down
8 changes: 7 additions & 1 deletion disnake/bans.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

from __future__ import annotations

from typing import TYPE_CHECKING, NamedTuple, Optional
from typing import TYPE_CHECKING, NamedTuple, Optional, Sequence

__all__ = ("BanEntry",)

if TYPE_CHECKING:
from .abc import Snowflake
from .user import User


class BanEntry(NamedTuple):
reason: Optional[str]
user: "User"


class BulkBanResult(NamedTuple):
banned: Sequence[Snowflake]
failed: Sequence[Snowflake]
4 changes: 4 additions & 0 deletions disnake/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@ class MessageType(Enum):
stage_speaker = 29
stage_topic = 31
guild_application_premium_subscription = 32
guild_incident_alert_mode_enabled = 36
guild_incident_alert_mode_disabled = 37
guild_incident_report_raid = 38
guild_incident_report_false_alarm = 39


class PartyType(Enum):
Expand Down
75 changes: 74 additions & 1 deletion disnake/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Any,
ClassVar,
Dict,
Iterable,
List,
Literal,
NamedTuple,
Expand All @@ -27,7 +28,7 @@
from .app_commands import GuildApplicationCommandPermissions
from .asset import Asset
from .automod import AutoModAction, AutoModRule
from .bans import BanEntry
from .bans import BanEntry, BulkBanResult
from .channel import (
CategoryChannel,
ForumChannel,
Expand Down Expand Up @@ -68,6 +69,7 @@
from .iterators import AuditLogIterator, BanIterator, MemberIterator
from .member import Member, VoiceState
from .mixins import Hashable
from .object import Object
from .onboarding import Onboarding
from .partial_emoji import PartialEmoji
from .permissions import PermissionOverwrite
Expand Down Expand Up @@ -4007,6 +4009,77 @@ async def unban(self, user: Snowflake, *, reason: Optional[str] = None) -> None:
"""
await self._state.http.unban(user.id, self.id, reason=reason)

async def bulk_ban(
self,
users: Iterable[Snowflake],
*,
clean_history_duration: Union[int, datetime.timedelta] = 0,
reason: Optional[str] = None,
) -> BulkBanResult:
"""|coro|
Bans multiple users from the guild at once.
The users must meet the :class:`abc.Snowflake` abc.
You must have :attr:`~Permissions.ban_members` and :attr:`~Permissions.manage_guild`
permissions to do this.
.. versionadded:: 2.10
Parameters
----------
users: Iterable[:class:`abc.Snowflake`]
The users to ban from the guild, up to 200.
clean_history_duration: Union[:class:`int`, :class:`datetime.timedelta`]
The timespan (seconds or timedelta) of messages to delete from the users
in the guild, up to 7 days (604800 seconds).
Defaults to ``0``.
.. note::
This may not be accurate with small durations (e.g. a few minutes)
and delete a couple minutes' worth of messages more than specified.
reason: Optional[:class:`str`]
The reason for banning the users. Shows up on the audit log.
Raises
------
TypeError
``clean_history_duration`` has an invalid type.
Forbidden
You do not have the proper permissions to bulk ban.
HTTPException
Banning failed. This is also raised if none of the users could be banned.
Returns
-------
:class:`BulkBanResult`
An object containing the successful and failed bans.
"""
if isinstance(clean_history_duration, datetime.timedelta):
delete_message_seconds = int(clean_history_duration.total_seconds())
elif isinstance(clean_history_duration, int):
delete_message_seconds = clean_history_duration
else:
raise TypeError(
"`clean_history_duration` should be int or timedelta, "
f"not {type(clean_history_duration).__name__}"
)

data = await self._state.http.bulk_ban(
[user.id for user in users],
self.id,
delete_message_seconds=delete_message_seconds,
reason=reason,
)

return BulkBanResult(
# these keys should always exist, but have a fallback just in case
[Object(u) for u in (data.get("banned_users") or [])],
[Object(u) for u in (data.get("failed_users") or [])],
)

async def vanity_invite(self, *, use_cached: bool = False) -> Optional[Invite]:
"""|coro|
Expand Down
16 changes: 16 additions & 0 deletions disnake/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,22 @@ def unban(
r = Route("DELETE", "/guilds/{guild_id}/bans/{user_id}", guild_id=guild_id, user_id=user_id)
return self.request(r, reason=reason)

def bulk_ban(
self,
user_ids: List[Snowflake],
guild_id: Snowflake,
*,
delete_message_seconds: int = 0,
reason: Optional[str] = None,
) -> Response[guild.BulkBanResult]:
r = Route("POST", "/guilds/{guild_id}/bulk-ban", guild_id=guild_id)
payload = {
"user_ids": user_ids,
"delete_message_seconds": delete_message_seconds,
}

return self.request(r, json=payload, reason=reason)

def get_guild_voice_regions(self, guild_id: Snowflake) -> Response[List[voice.VoiceRegion]]:
return self.request(Route("GET", "/guilds/{guild_id}/regions", guild_id=guild_id))

Expand Down
111 changes: 89 additions & 22 deletions disnake/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
MemberWithUser as MemberWithUserPayload,
UserWithMember as UserWithMemberPayload,
)
from .types.user import User as UserPayload
from .types.user import AvatarDecorationData as AvatarDecorationDataPayload, User as UserPayload
from .types.voice import (
GuildVoiceState as GuildVoiceStatePayload,
VoiceState as VoiceStatePayload,
Expand Down Expand Up @@ -274,6 +274,7 @@ class Member(disnake.abc.Messageable, _UserTag):
"_avatar",
"_communication_disabled_until",
"_flags",
"_avatar_decoration_data",
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -342,6 +343,9 @@ def __init__(
timeout_datetime = utils.parse_time(data.get("communication_disabled_until"))
self._communication_disabled_until: Optional[datetime.datetime] = timeout_datetime
self._flags: int = data.get("flags", 0)
self._avatar_decoration_data: Optional[AvatarDecorationDataPayload] = data.get(
"avatar_decoration_data"
)

def __str__(self) -> str:
return str(self._user)
Expand Down Expand Up @@ -436,6 +440,7 @@ def _update(self, data: GuildMemberUpdateEvent) -> None:
timeout_datetime = utils.parse_time(data.get("communication_disabled_until"))
self._communication_disabled_until = timeout_datetime
self._flags = data.get("flags", 0)
self._avatar_decoration_data = data.get("avatar_decoration_data")

def _presence_update(
self, data: PresenceData, user: UserPayload
Expand All @@ -452,18 +457,33 @@ def _presence_update(

def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
u = self._user
original = (u.name, u._avatar, u.discriminator, u.global_name, u._public_flags)
original = (
u.name,
u._avatar,
u.discriminator,
u.global_name,
u._public_flags,
u._avatar_decoration_data,
)
# These keys seem to always be available
modified = (
user["username"],
user["avatar"],
user["discriminator"],
user.get("global_name"),
user.get("public_flags", 0),
user.get("avatar_decoration_data", None),
)
if original != modified:
to_return = User._copy(self._user)
u.name, u._avatar, u.discriminator, u.global_name, u._public_flags = modified
(
u.name,
u._avatar,
u.discriminator,
u.global_name,
u._public_flags,
u._avatar_decoration_data,
) = modified
# Signal to dispatch on_user_update
return to_return, u

Expand Down Expand Up @@ -718,6 +738,49 @@ def flags(self) -> MemberFlags:
"""
return MemberFlags._from_value(self._flags)

@property
def display_avatar_decoration(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the member's display avatar decoration.
For regular members this is just their avatar decoration, but
if they have a guild specific avatar decoration then that
is returned instead.
.. versionadded:: 2.10
.. note::
Since Discord always sends an animated PNG for animated avatar decorations,
the following methods will not work as expected:
- :meth:`Asset.replace`
- :meth:`Asset.with_size`
- :meth:`Asset.with_format`
- :meth:`Asset.with_static_format`
"""
return self.guild_avatar_decoration or self._user.avatar_decoration

@property
def guild_avatar_decoration(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild avatar decoration
the member has. If unavailable, ``None`` is returned.
.. versionadded:: 2.10
.. note::
Since Discord always sends an animated PNG for animated avatar decorations,
the following methods will not work as expected:
- :meth:`Asset.replace`
- :meth:`Asset.with_size`
- :meth:`Asset.with_format`
- :meth:`Asset.with_static_format`
"""
if self._avatar_decoration_data is None:
return None
return Asset._from_avatar_decoration(self._state, self._avatar_decoration_data["asset"])

@overload
async def ban(
self,
Expand Down Expand Up @@ -788,25 +851,29 @@ async def edit(
Depending on the parameter passed, this requires different permissions listed below:
+------------------------------+-------------------------------------+
| Parameter | Permission |
+------------------------------+-------------------------------------+
| nick | :attr:`Permissions.manage_nicknames`|
+------------------------------+-------------------------------------+
| mute | :attr:`Permissions.mute_members` |
+------------------------------+-------------------------------------+
| deafen | :attr:`Permissions.deafen_members` |
+------------------------------+-------------------------------------+
| roles | :attr:`Permissions.manage_roles` |
+------------------------------+-------------------------------------+
| voice_channel | :attr:`Permissions.move_members` |
+------------------------------+-------------------------------------+
| timeout | :attr:`Permissions.moderate_members`|
+------------------------------+-------------------------------------+
| flags | :attr:`Permissions.moderate_members`|
+------------------------------+-------------------------------------+
| bypasses_verification | :attr:`Permissions.moderate_members`|
+------------------------------+-------------------------------------+
+------------------------------+--------------------------------------+
| Parameter | Permission |
+==============================+======================================+
| nick | :attr:`Permissions.manage_nicknames` |
+------------------------------+--------------------------------------+
| mute | :attr:`Permissions.mute_members` |
+------------------------------+--------------------------------------+
| deafen | :attr:`Permissions.deafen_members` |
+------------------------------+--------------------------------------+
| roles | :attr:`Permissions.manage_roles` |
+------------------------------+--------------------------------------+
| voice_channel | :attr:`Permissions.move_members` |
+------------------------------+--------------------------------------+
| timeout | :attr:`Permissions.moderate_members` |
+------------------------------+--------------------------------------+
| flags | :attr:`Permissions.manage_guild` or |
| | :attr:`Permissions.manage_roles` or |
| | (:attr:`Permissions.moderate_members`|
| | + :attr:`Permissions.kick_members` |
| | + :attr:`Permissions.ban_members`) |
+------------------------------+--------------------------------------+
| bypasses_verification | (same as ``flags``) |
+------------------------------+--------------------------------------+
All parameters are optional.
Expand Down
14 changes: 14 additions & 0 deletions disnake/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,20 @@ def system_content(self) -> Optional[str]:
)
return f"{self.author.name} upgraded {application_name} to premium for this server! 🎉"

if self.type is MessageType.guild_incident_alert_mode_enabled:
enabled_until = utils.parse_time(self.content)
return f"{self.author.name} enabled security actions until {enabled_until.strftime('%d/%m/%Y, %H:%M')}."

if self.type is MessageType.guild_incident_alert_mode_disabled:
return f"{self.author.name} disabled security actions."

if self.type is MessageType.guild_incident_report_raid:
guild_name = self.guild.name if self.guild else None
return f"{self.author.name} reported a raid in {guild_name}."

if self.type is MessageType.guild_incident_report_false_alarm:
return f"{self.author.name} resolved an Activity Alert."

# in the event of an unknown or unsupported message type, we return nothing
return None

Expand Down
Loading

0 comments on commit fa81c0f

Please sign in to comment.