Browse Source

Merge branch 'master' of https://github.com/Soheab/discord.py into guide/embeds

pull/9708/head
Soheab_ 1 year ago
parent
commit
c71df166cc
  1. 16
      discord/abc.py
  2. 2
      discord/app_commands/commands.py
  3. 9
      discord/asset.py
  4. 8
      discord/components.py
  5. 2
      discord/gateway.py
  6. 85
      discord/guild.py
  7. 3
      discord/http.py
  8. 29
      discord/member.py
  9. 3
      discord/types/gateway.py
  10. 6
      discord/types/guild.py
  11. 5
      discord/types/member.py
  12. 7
      discord/types/user.py
  13. 4
      discord/ui/button.py
  14. 3
      discord/ui/modal.py
  15. 22
      discord/ui/select.py
  16. 6
      discord/ui/text_input.py
  17. 35
      discord/user.py
  18. 17
      discord/voice_state.py
  19. 3
      discord/webhook/async_.py
  20. 1
      setup.py

16
discord/abc.py

@ -248,6 +248,22 @@ class User(Snowflake, Protocol):
"""Optional[:class:`~discord.Asset`]: Returns an Asset that represents the user's avatar, if present.""" """Optional[:class:`~discord.Asset`]: Returns an Asset that represents the user's avatar, if present."""
raise NotImplementedError raise NotImplementedError
@property
def avatar_decoration(self) -> Optional[Asset]:
"""Optional[:class:`~discord.Asset`]: Returns an Asset that represents the user's avatar decoration, if present.
.. versionadded:: 2.4
"""
raise NotImplementedError
@property
def avatar_decoration_sku_id(self) -> Optional[int]:
"""Optional[:class:`int`]: Returns an integer that represents the user's avatar decoration SKU ID, if present.
.. versionadded:: 2.4
"""
raise NotImplementedError
@property @property
def default_avatar(self) -> Asset: def default_avatar(self) -> Asset:
""":class:`~discord.Asset`: Returns the default avatar for a given user.""" """:class:`~discord.Asset`: Returns the default avatar for a given user."""

2
discord/app_commands/commands.py

@ -1068,7 +1068,7 @@ class Command(Generic[GroupT, P, T]):
def decorator(coro: AutocompleteCallback[GroupT, ChoiceT]) -> AutocompleteCallback[GroupT, ChoiceT]: def decorator(coro: AutocompleteCallback[GroupT, ChoiceT]) -> AutocompleteCallback[GroupT, ChoiceT]:
if not inspect.iscoroutinefunction(coro): if not inspect.iscoroutinefunction(coro):
raise TypeError('The error handler must be a coroutine.') raise TypeError('The autocomplete callback must be a coroutine function.')
try: try:
param = self._params[name] param = self._params[name]

9
discord/asset.py

@ -246,6 +246,15 @@ class Asset(AssetMixin):
animated=animated, animated=animated,
) )
@classmethod
def _from_avatar_decoration(cls, state: _State, avatar_decoration: str) -> Self:
return cls(
state,
url=f'{cls.BASE}/avatar-decoration-presets/{avatar_decoration}.png?size=96',
key=avatar_decoration,
animated=True,
)
@classmethod @classmethod
def _from_icon(cls, state: _State, object_id: int, icon_hash: str, path: str) -> Self: def _from_icon(cls, state: _State, object_id: int, icon_hash: str, path: str) -> Self:
return cls( return cls(

8
discord/components.py

@ -318,8 +318,8 @@ class SelectOption:
Can only be up to 100 characters. Can only be up to 100 characters.
value: :class:`str` value: :class:`str`
The value of the option. This is not displayed to users. The value of the option. This is not displayed to users.
If not provided when constructed then it defaults to the If not provided when constructed then it defaults to the label.
label. Can only be up to 100 characters. Can only be up to 100 characters.
description: Optional[:class:`str`] description: Optional[:class:`str`]
An additional description of the option, if any. An additional description of the option, if any.
Can only be up to 100 characters. Can only be up to 100 characters.
@ -332,14 +332,12 @@ class SelectOption:
----------- -----------
label: :class:`str` label: :class:`str`
The label of the option. This is displayed to users. The label of the option. This is displayed to users.
Can only be up to 100 characters.
value: :class:`str` value: :class:`str`
The value of the option. This is not displayed to users. The value of the option. This is not displayed to users.
If not provided when constructed then it defaults to the If not provided when constructed then it defaults to the
label. Can only be up to 100 characters. label.
description: Optional[:class:`str`] description: Optional[:class:`str`]
An additional description of the option, if any. An additional description of the option, if any.
Can only be up to 100 characters.
default: :class:`bool` default: :class:`bool`
Whether this option is selected by default. Whether this option is selected by default.
""" """

2
discord/gateway.py

@ -827,7 +827,7 @@ class DiscordVoiceWebSocket:
self.loop: asyncio.AbstractEventLoop = loop self.loop: asyncio.AbstractEventLoop = loop
self._keep_alive: Optional[VoiceKeepAliveHandler] = None self._keep_alive: Optional[VoiceKeepAliveHandler] = None
self._close_code: Optional[int] = None self._close_code: Optional[int] = None
self.secret_key: Optional[str] = None self.secret_key: Optional[List[int]] = None
if hook: if hook:
self._hook = hook self._hook = hook

85
discord/guild.py

@ -109,6 +109,7 @@ if TYPE_CHECKING:
Guild as GuildPayload, Guild as GuildPayload,
RolePositionUpdate as RolePositionUpdatePayload, RolePositionUpdate as RolePositionUpdatePayload,
GuildFeature, GuildFeature,
IncidentData,
) )
from .types.threads import ( from .types.threads import (
Thread as ThreadPayload, Thread as ThreadPayload,
@ -320,6 +321,7 @@ class Guild(Hashable):
'premium_progress_bar_enabled', 'premium_progress_bar_enabled',
'_safety_alerts_channel_id', '_safety_alerts_channel_id',
'max_stage_video_users', 'max_stage_video_users',
'_incidents_data',
) )
_PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = { _PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = {
@ -509,6 +511,7 @@ class Guild(Hashable):
self.owner_id: Optional[int] = utils._get_as_snowflake(guild, 'owner_id') self.owner_id: Optional[int] = utils._get_as_snowflake(guild, 'owner_id')
self._large: Optional[bool] = None if self._member_count is None else self._member_count >= 250 self._large: Optional[bool] = None if self._member_count is None else self._member_count >= 250
self._afk_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'afk_channel_id') self._afk_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'afk_channel_id')
self._incidents_data: Optional[IncidentData] = guild.get('incidents_data')
if 'channels' in guild: if 'channels' in guild:
channels = guild['channels'] channels = guild['channels']
@ -1843,6 +1846,8 @@ class Guild(Hashable):
mfa_level: MFALevel = MISSING, mfa_level: MFALevel = MISSING,
raid_alerts_disabled: bool = MISSING, raid_alerts_disabled: bool = MISSING,
safety_alerts_channel: TextChannel = MISSING, safety_alerts_channel: TextChannel = MISSING,
invites_disabled_until: datetime.datetime = MISSING,
dms_disabled_until: datetime.datetime = MISSING,
) -> Guild: ) -> Guild:
r"""|coro| r"""|coro|
@ -1969,6 +1974,18 @@ class Guild(Hashable):
.. versionadded:: 2.3 .. versionadded:: 2.3
invites_disabled_until: Optional[:class:`datetime.datetime`]
The time when invites should be enabled again, or ``None`` to disable the action.
This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`.
.. versionadded:: 2.4
dms_disabled_until: Optional[:class:`datetime.datetime`]
The time when direct messages should be allowed again, or ``None`` to disable the action.
This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`.
.. versionadded:: 2.4
Raises Raises
------- -------
Forbidden Forbidden
@ -2157,6 +2174,30 @@ class Guild(Hashable):
await http.edit_guild_mfa_level(self.id, mfa_level=mfa_level.value) await http.edit_guild_mfa_level(self.id, mfa_level=mfa_level.value)
incident_actions_payload: IncidentData = {}
if invites_disabled_until is not MISSING:
if invites_disabled_until is None:
incident_actions_payload['invites_disabled_until'] = None
else:
if invites_disabled_until.tzinfo is None:
raise TypeError(
'invites_disabled_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.'
)
incident_actions_payload['invites_disabled_until'] = invites_disabled_until.isoformat()
if dms_disabled_until is not MISSING:
if dms_disabled_until is None:
incident_actions_payload['dms_disabled_until'] = None
else:
if dms_disabled_until.tzinfo is None:
raise TypeError(
'dms_disabled_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.'
)
incident_actions_payload['dms_disabled_until'] = dms_disabled_until.isoformat()
if incident_actions_payload:
await http.edit_incident_actions(self.id, payload=incident_actions_payload)
data = await http.edit_guild(self.id, reason=reason, **fields) data = await http.edit_guild(self.id, reason=reason, **fields)
return Guild(data=data, state=self._state) return Guild(data=data, state=self._state)
@ -4292,3 +4333,47 @@ class Guild(Hashable):
) )
return AutoModRule(data=data, guild=self, state=self._state) return AutoModRule(data=data, guild=self, state=self._state)
@property
def invites_paused_until(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: If invites are paused, returns when
invites will get enabled in UTC, otherwise returns None.
.. versionadded:: 2.4
"""
if not self._incidents_data:
return None
return utils.parse_time(self._incidents_data.get('invites_disabled_until'))
@property
def dms_paused_until(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: If DMs are paused, returns when DMs
will get enabled in UTC, otherwise returns None.
.. versionadded:: 2.4
"""
if not self._incidents_data:
return None
return utils.parse_time(self._incidents_data.get('dms_disabled_until'))
def invites_paused(self) -> bool:
""":class:`bool`: Whether invites are paused in the guild.
.. versionadded:: 2.4
"""
if not self.invites_paused_until:
return False
return self.invites_paused_until > utils.utcnow()
def dms_paused(self) -> bool:
""":class:`bool`: Whether DMs are paused in the guild.
.. versionadded:: 2.4
"""
if not self.dms_paused_until:
return False
return self.dms_paused_until > utils.utcnow()

3
discord/http.py

@ -1764,6 +1764,9 @@ class HTTPClient:
) -> Response[widget.WidgetSettings]: ) -> Response[widget.WidgetSettings]:
return self.request(Route('PATCH', '/guilds/{guild_id}/widget', guild_id=guild_id), json=payload, reason=reason) return self.request(Route('PATCH', '/guilds/{guild_id}/widget', guild_id=guild_id), json=payload, reason=reason)
def edit_incident_actions(self, guild_id: Snowflake, payload: guild.IncidentData) -> Response[guild.IncidentData]:
return self.request(Route('PUT', '/guilds/{guild_id}/incident-actions', guild_id=guild_id), json=payload)
# Invite management # Invite management
def create_invite( def create_invite(

29
discord/member.py

@ -67,7 +67,7 @@ if TYPE_CHECKING:
UserWithMember as UserWithMemberPayload, UserWithMember as UserWithMemberPayload,
) )
from .types.gateway import GuildMemberUpdateEvent from .types.gateway import GuildMemberUpdateEvent
from .types.user import User as UserPayload from .types.user import User as UserPayload, AvatarDecorationData
from .abc import Snowflake from .abc import Snowflake
from .state import ConnectionState from .state import ConnectionState
from .message import Message from .message import Message
@ -323,6 +323,7 @@ class Member(discord.abc.Messageable, _UserTag):
'_state', '_state',
'_avatar', '_avatar',
'_flags', '_flags',
'_avatar_decoration_data',
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@ -342,6 +343,8 @@ class Member(discord.abc.Messageable, _UserTag):
banner: Optional[Asset] banner: Optional[Asset]
accent_color: Optional[Colour] accent_color: Optional[Colour]
accent_colour: Optional[Colour] accent_colour: Optional[Colour]
avatar_decoration: Optional[Asset]
avatar_decoration_sku_id: Optional[int]
def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState): def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState):
self._state: ConnectionState = state self._state: ConnectionState = state
@ -357,6 +360,7 @@ class Member(discord.abc.Messageable, _UserTag):
self._avatar: Optional[str] = data.get('avatar') self._avatar: Optional[str] = data.get('avatar')
self._permissions: Optional[int] self._permissions: Optional[int]
self._flags: int = data['flags'] self._flags: int = data['flags']
self._avatar_decoration_data: Optional[AvatarDecorationData] = data.get('avatar_decoration_data')
try: try:
self._permissions = int(data['permissions']) self._permissions = int(data['permissions'])
except KeyError: except KeyError:
@ -425,6 +429,7 @@ class Member(discord.abc.Messageable, _UserTag):
self._permissions = member._permissions self._permissions = member._permissions
self._state = member._state self._state = member._state
self._avatar = member._avatar self._avatar = member._avatar
self._avatar_decoration_data = member._avatar_decoration_data
# Reference will not be copied unless necessary by PRESENCE_UPDATE # Reference will not be copied unless necessary by PRESENCE_UPDATE
# See below # See below
@ -453,6 +458,7 @@ class Member(discord.abc.Messageable, _UserTag):
self._roles = utils.SnowflakeList(map(int, data['roles'])) self._roles = utils.SnowflakeList(map(int, data['roles']))
self._avatar = data.get('avatar') self._avatar = data.get('avatar')
self._flags = data.get('flags', 0) self._flags = data.get('flags', 0)
self._avatar_decoration_data = data.get('avatar_decoration_data')
def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]: def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]:
self.activities = tuple(create_activity(d, self._state) for d in data['activities']) self.activities = tuple(create_activity(d, self._state) for d in data['activities'])
@ -464,7 +470,16 @@ class Member(discord.abc.Messageable, _UserTag):
def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
u = self._user u = self._user
original = (u.name, u.discriminator, u._avatar, u.global_name, u._public_flags) original = (
u.name,
u.discriminator,
u._avatar,
u.global_name,
u._public_flags,
u._avatar_decoration_data['sku_id'] if u._avatar_decoration_data is not None else None,
)
decoration_payload = user.get('avatar_decoration_data')
# These keys seem to always be available # These keys seem to always be available
modified = ( modified = (
user['username'], user['username'],
@ -472,10 +487,18 @@ class Member(discord.abc.Messageable, _UserTag):
user['avatar'], user['avatar'],
user.get('global_name'), user.get('global_name'),
user.get('public_flags', 0), user.get('public_flags', 0),
decoration_payload['sku_id'] if decoration_payload is not None else None,
) )
if original != modified: if original != modified:
to_return = User._copy(self._user) to_return = User._copy(self._user)
u.name, u.discriminator, u._avatar, u.global_name, u._public_flags = modified u.name, u.discriminator, u._avatar, u.global_name, u._public_flags, u._avatar_decoration_data = (
user['username'],
user['discriminator'],
user['avatar'],
user.get('global_name'),
user.get('public_flags', 0),
decoration_payload,
)
# Signal to dispatch on_user_update # Signal to dispatch on_user_update
return to_return, u return to_return, u

3
discord/types/gateway.py

@ -41,7 +41,7 @@ from .message import Message
from .sticker import GuildSticker from .sticker import GuildSticker
from .appinfo import GatewayAppInfo, PartialAppInfo from .appinfo import GatewayAppInfo, PartialAppInfo
from .guild import Guild, UnavailableGuild from .guild import Guild, UnavailableGuild
from .user import User from .user import User, AvatarDecorationData
from .threads import Thread, ThreadMember from .threads import Thread, ThreadMember
from .scheduled_event import GuildScheduledEvent from .scheduled_event import GuildScheduledEvent
from .audit_log import AuditLogEntry from .audit_log import AuditLogEntry
@ -228,6 +228,7 @@ class GuildMemberUpdateEvent(TypedDict):
mute: NotRequired[bool] mute: NotRequired[bool]
pending: NotRequired[bool] pending: NotRequired[bool]
communication_disabled_until: NotRequired[str] communication_disabled_until: NotRequired[str]
avatar_decoration_data: NotRequired[AvatarDecorationData]
class GuildEmojisUpdateEvent(TypedDict): class GuildEmojisUpdateEvent(TypedDict):

6
discord/types/guild.py

@ -49,6 +49,11 @@ class UnavailableGuild(TypedDict):
unavailable: NotRequired[bool] unavailable: NotRequired[bool]
class IncidentData(TypedDict):
invites_disabled_until: NotRequired[Optional[str]]
dms_disabled_until: NotRequired[Optional[str]]
DefaultMessageNotificationLevel = Literal[0, 1] DefaultMessageNotificationLevel = Literal[0, 1]
ExplicitContentFilterLevel = Literal[0, 1, 2] ExplicitContentFilterLevel = Literal[0, 1, 2]
MFALevel = Literal[0, 1] MFALevel = Literal[0, 1]
@ -97,6 +102,7 @@ class _BaseGuildPreview(UnavailableGuild):
stickers: List[GuildSticker] stickers: List[GuildSticker]
features: List[GuildFeature] features: List[GuildFeature]
description: Optional[str] description: Optional[str]
incidents_data: Optional[IncidentData]
class _GuildPreviewUnique(TypedDict): class _GuildPreviewUnique(TypedDict):

5
discord/types/member.py

@ -24,7 +24,8 @@ DEALINGS IN THE SOFTWARE.
from typing import Optional, TypedDict from typing import Optional, TypedDict
from .snowflake import SnowflakeList from .snowflake import SnowflakeList
from .user import User from .user import User, AvatarDecorationData
from typing_extensions import NotRequired
class Nickname(TypedDict): class Nickname(TypedDict):
@ -47,6 +48,7 @@ class Member(PartialMember, total=False):
pending: bool pending: bool
permissions: str permissions: str
communication_disabled_until: str communication_disabled_until: str
avatar_decoration_data: NotRequired[AvatarDecorationData]
class _OptionalMemberWithUser(PartialMember, total=False): class _OptionalMemberWithUser(PartialMember, total=False):
@ -56,6 +58,7 @@ class _OptionalMemberWithUser(PartialMember, total=False):
pending: bool pending: bool
permissions: str permissions: str
communication_disabled_until: str communication_disabled_until: str
avatar_decoration_data: NotRequired[AvatarDecorationData]
class MemberWithUser(_OptionalMemberWithUser): class MemberWithUser(_OptionalMemberWithUser):

7
discord/types/user.py

@ -24,6 +24,12 @@ DEALINGS IN THE SOFTWARE.
from .snowflake import Snowflake from .snowflake import Snowflake
from typing import Literal, Optional, TypedDict from typing import Literal, Optional, TypedDict
from typing_extensions import NotRequired
class AvatarDecorationData(TypedDict):
asset: str
sku_id: Snowflake
class PartialUser(TypedDict): class PartialUser(TypedDict):
@ -32,6 +38,7 @@ class PartialUser(TypedDict):
discriminator: str discriminator: str
avatar: Optional[str] avatar: Optional[str]
global_name: Optional[str] global_name: Optional[str]
avatar_decoration_data: NotRequired[AvatarDecorationData]
PremiumType = Literal[0, 1, 2, 3] PremiumType = Literal[0, 1, 2, 3]

4
discord/ui/button.py

@ -61,12 +61,14 @@ class Button(Item[V]):
custom_id: Optional[:class:`str`] custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction. The ID of the button that gets received during an interaction.
If this button is for a URL, it does not have a custom ID. If this button is for a URL, it does not have a custom ID.
Can only be up to 100 characters.
url: Optional[:class:`str`] url: Optional[:class:`str`]
The URL this button sends you to. The URL this button sends you to.
disabled: :class:`bool` disabled: :class:`bool`
Whether the button is disabled or not. Whether the button is disabled or not.
label: Optional[:class:`str`] label: Optional[:class:`str`]
The label of the button, if any. The label of the button, if any.
Can only be up to 80 characters.
emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]] emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]]
The emoji of the button, if available. The emoji of the button, if available.
row: Optional[:class:`int`] row: Optional[:class:`int`]
@ -258,9 +260,11 @@ def button(
------------ ------------
label: Optional[:class:`str`] label: Optional[:class:`str`]
The label of the button, if any. The label of the button, if any.
Can only be up to 80 characters.
custom_id: Optional[:class:`str`] custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction. The ID of the button that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts. It is recommended not to set this parameter to prevent conflicts.
Can only be up to 100 characters.
style: :class:`.ButtonStyle` style: :class:`.ButtonStyle`
The style of the button. Defaults to :attr:`.ButtonStyle.grey`. The style of the button. Defaults to :attr:`.ButtonStyle.grey`.
disabled: :class:`bool` disabled: :class:`bool`

3
discord/ui/modal.py

@ -77,7 +77,8 @@ class Modal(View):
Parameters Parameters
----------- -----------
title: :class:`str` title: :class:`str`
The title of the modal. Can only be up to 45 characters. The title of the modal.
Can only be up to 45 characters.
timeout: Optional[:class:`float`] timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the UI before no longer accepting input. Timeout in seconds from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout. If ``None`` then there is no timeout.

22
discord/ui/select.py

@ -366,8 +366,10 @@ class Select(BaseSelect[V]):
custom_id: :class:`str` custom_id: :class:`str`
The ID of the select menu that gets received during an interaction. The ID of the select menu that gets received during an interaction.
If not given then one is generated for you. If not given then one is generated for you.
Can only be up to 100 characters.
placeholder: Optional[:class:`str`] placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any. The placeholder text that is shown if nothing is selected, if any.
Can only be up to 150 characters.
min_values: :class:`int` min_values: :class:`int`
The minimum number of items that must be chosen for this select menu. The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25. Defaults to 1 and must be between 0 and 25.
@ -376,6 +378,7 @@ class Select(BaseSelect[V]):
Defaults to 1 and must be between 1 and 25. Defaults to 1 and must be between 1 and 25.
options: List[:class:`discord.SelectOption`] options: List[:class:`discord.SelectOption`]
A list of options that can be selected in this menu. A list of options that can be selected in this menu.
Can only contain up to 25 items.
disabled: :class:`bool` disabled: :class:`bool`
Whether the select is disabled or not. Whether the select is disabled or not.
row: Optional[:class:`int`] row: Optional[:class:`int`]
@ -455,7 +458,8 @@ class Select(BaseSelect[V]):
Can only be up to 100 characters. Can only be up to 100 characters.
value: :class:`str` value: :class:`str`
The value of the option. This is not displayed to users. The value of the option. This is not displayed to users.
If not given, defaults to the label. Can only be up to 100 characters. If not given, defaults to the label.
Can only be up to 100 characters.
description: Optional[:class:`str`] description: Optional[:class:`str`]
An additional description of the option, if any. An additional description of the option, if any.
Can only be up to 100 characters. Can only be up to 100 characters.
@ -515,8 +519,10 @@ class UserSelect(BaseSelect[V]):
custom_id: :class:`str` custom_id: :class:`str`
The ID of the select menu that gets received during an interaction. The ID of the select menu that gets received during an interaction.
If not given then one is generated for you. If not given then one is generated for you.
Can only be up to 100 characters.
placeholder: Optional[:class:`str`] placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any. The placeholder text that is shown if nothing is selected, if any.
Can only be up to 150 characters.
min_values: :class:`int` min_values: :class:`int`
The minimum number of items that must be chosen for this select menu. The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25. Defaults to 1 and must be between 0 and 25.
@ -527,6 +533,7 @@ class UserSelect(BaseSelect[V]):
Whether the select is disabled or not. Whether the select is disabled or not.
default_values: Sequence[:class:`~discord.abc.Snowflake`] default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the users that should be selected by default. A list of objects representing the users that should be selected by default.
Number of items must be in range of ``min_values`` and ``max_values``.
.. versionadded:: 2.4 .. versionadded:: 2.4
row: Optional[:class:`int`] row: Optional[:class:`int`]
@ -604,8 +611,10 @@ class RoleSelect(BaseSelect[V]):
custom_id: :class:`str` custom_id: :class:`str`
The ID of the select menu that gets received during an interaction. The ID of the select menu that gets received during an interaction.
If not given then one is generated for you. If not given then one is generated for you.
Can only be up to 100 characters.
placeholder: Optional[:class:`str`] placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any. The placeholder text that is shown if nothing is selected, if any.
Can only be up to 150 characters.
min_values: :class:`int` min_values: :class:`int`
The minimum number of items that must be chosen for this select menu. The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25. Defaults to 1 and must be between 0 and 25.
@ -616,6 +625,7 @@ class RoleSelect(BaseSelect[V]):
Whether the select is disabled or not. Whether the select is disabled or not.
default_values: Sequence[:class:`~discord.abc.Snowflake`] default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the roles that should be selected by default. A list of objects representing the roles that should be selected by default.
Number of items must be in range of ``min_values`` and ``max_values``.
.. versionadded:: 2.4 .. versionadded:: 2.4
row: Optional[:class:`int`] row: Optional[:class:`int`]
@ -688,8 +698,10 @@ class MentionableSelect(BaseSelect[V]):
custom_id: :class:`str` custom_id: :class:`str`
The ID of the select menu that gets received during an interaction. The ID of the select menu that gets received during an interaction.
If not given then one is generated for you. If not given then one is generated for you.
Can only be up to 100 characters.
placeholder: Optional[:class:`str`] placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any. The placeholder text that is shown if nothing is selected, if any.
Can only be up to 150 characters.
min_values: :class:`int` min_values: :class:`int`
The minimum number of items that must be chosen for this select menu. The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25. Defaults to 1 and must be between 0 and 25.
@ -701,6 +713,7 @@ class MentionableSelect(BaseSelect[V]):
default_values: Sequence[:class:`~discord.abc.Snowflake`] default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the users/roles that should be selected by default. A list of objects representing the users/roles that should be selected by default.
if :class:`.Object` is passed, then the type must be specified in the constructor. if :class:`.Object` is passed, then the type must be specified in the constructor.
Number of items must be in range of ``min_values`` and ``max_values``.
.. versionadded:: 2.4 .. versionadded:: 2.4
row: Optional[:class:`int`] row: Optional[:class:`int`]
@ -778,10 +791,12 @@ class ChannelSelect(BaseSelect[V]):
custom_id: :class:`str` custom_id: :class:`str`
The ID of the select menu that gets received during an interaction. The ID of the select menu that gets received during an interaction.
If not given then one is generated for you. If not given then one is generated for you.
Can only be up to 100 characters.
channel_types: List[:class:`~discord.ChannelType`] channel_types: List[:class:`~discord.ChannelType`]
The types of channels to show in the select menu. Defaults to all channels. The types of channels to show in the select menu. Defaults to all channels.
placeholder: Optional[:class:`str`] placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any. The placeholder text that is shown if nothing is selected, if any.
Can only be up to 150 characters.
min_values: :class:`int` min_values: :class:`int`
The minimum number of items that must be chosen for this select menu. The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25. Defaults to 1 and must be between 0 and 25.
@ -792,6 +807,7 @@ class ChannelSelect(BaseSelect[V]):
Whether the select is disabled or not. Whether the select is disabled or not.
default_values: Sequence[:class:`~discord.abc.Snowflake`] default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the channels that should be selected by default. A list of objects representing the channels that should be selected by default.
Number of items must be in range of ``min_values`` and ``max_values``.
.. versionadded:: 2.4 .. versionadded:: 2.4
row: Optional[:class:`int`] row: Optional[:class:`int`]
@ -1011,9 +1027,11 @@ def select(
get overridden. get overridden.
placeholder: Optional[:class:`str`] placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any. The placeholder text that is shown if nothing is selected, if any.
Can only be up to 150 characters.
custom_id: :class:`str` custom_id: :class:`str`
The ID of the select menu that gets received during an interaction. The ID of the select menu that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts. It is recommended not to set this parameter to prevent conflicts.
Can only be up to 100 characters.
row: Optional[:class:`int`] row: Optional[:class:`int`]
The relative row this select menu belongs to. A Discord component can only have 5 The relative row this select menu belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd rows. By default, items are arranged automatically into those 5 rows. If you'd
@ -1029,6 +1047,7 @@ def select(
options: List[:class:`discord.SelectOption`] options: List[:class:`discord.SelectOption`]
A list of options that can be selected in this menu. This can only be used with A list of options that can be selected in this menu. This can only be used with
:class:`Select` instances. :class:`Select` instances.
Can only contain up to 25 items.
channel_types: List[:class:`~discord.ChannelType`] channel_types: List[:class:`~discord.ChannelType`]
The types of channels to show in the select menu. Defaults to all channels. This can only be used The types of channels to show in the select menu. Defaults to all channels. This can only be used
with :class:`ChannelSelect` instances. with :class:`ChannelSelect` instances.
@ -1037,6 +1056,7 @@ def select(
default_values: Sequence[:class:`~discord.abc.Snowflake`] default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the default values for the select menu. This cannot be used with regular :class:`Select` instances. A list of objects representing the default values for the select menu. This cannot be used with regular :class:`Select` instances.
If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor. If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor.
Number of items must be in range of ``min_values`` and ``max_values``.
.. versionadded:: 2.4 .. versionadded:: 2.4
""" """

6
discord/ui/text_input.py

@ -65,21 +65,27 @@ class TextInput(Item[V]):
------------ ------------
label: :class:`str` label: :class:`str`
The label to display above the text input. The label to display above the text input.
Can only be up to 45 characters.
custom_id: :class:`str` custom_id: :class:`str`
The ID of the text input that gets received during an interaction. The ID of the text input that gets received during an interaction.
If not given then one is generated for you. If not given then one is generated for you.
Can only be up to 100 characters.
style: :class:`discord.TextStyle` style: :class:`discord.TextStyle`
The style of the text input. The style of the text input.
placeholder: Optional[:class:`str`] placeholder: Optional[:class:`str`]
The placeholder text to display when the text input is empty. The placeholder text to display when the text input is empty.
Can only be up to 100 characters.
default: Optional[:class:`str`] default: Optional[:class:`str`]
The default value of the text input. The default value of the text input.
Can only be up to 4000 characters.
required: :class:`bool` required: :class:`bool`
Whether the text input is required. Whether the text input is required.
min_length: Optional[:class:`int`] min_length: Optional[:class:`int`]
The minimum length of the text input. The minimum length of the text input.
Must be between 0 and 4000.
max_length: Optional[:class:`int`] max_length: Optional[:class:`int`]
The maximum length of the text input. The maximum length of the text input.
Must be between 1 and 4000.
row: Optional[:class:`int`] row: Optional[:class:`int`]
The relative row this text input belongs to. A Discord component can only have 5 The relative row this text input belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd rows. By default, items are arranged automatically into those 5 rows. If you'd

35
discord/user.py

@ -31,7 +31,7 @@ from .asset import Asset
from .colour import Colour from .colour import Colour
from .enums import DefaultAvatar from .enums import DefaultAvatar
from .flags import PublicUserFlags from .flags import PublicUserFlags
from .utils import snowflake_time, _bytes_to_base64_data, MISSING from .utils import snowflake_time, _bytes_to_base64_data, MISSING, _get_as_snowflake
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
@ -43,10 +43,7 @@ if TYPE_CHECKING:
from .message import Message from .message import Message
from .state import ConnectionState from .state import ConnectionState
from .types.channel import DMChannel as DMChannelPayload from .types.channel import DMChannel as DMChannelPayload
from .types.user import ( from .types.user import PartialUser as PartialUserPayload, User as UserPayload, AvatarDecorationData
PartialUser as PartialUserPayload,
User as UserPayload,
)
__all__ = ( __all__ = (
@ -73,6 +70,7 @@ class BaseUser(_UserTag):
'system', 'system',
'_public_flags', '_public_flags',
'_state', '_state',
'_avatar_decoration_data',
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@ -87,6 +85,7 @@ class BaseUser(_UserTag):
_banner: Optional[str] _banner: Optional[str]
_accent_colour: Optional[int] _accent_colour: Optional[int]
_public_flags: int _public_flags: int
_avatar_decoration_data: Optional[AvatarDecorationData]
def __init__(self, *, state: ConnectionState, data: Union[UserPayload, PartialUserPayload]) -> None: def __init__(self, *, state: ConnectionState, data: Union[UserPayload, PartialUserPayload]) -> None:
self._state = state self._state = state
@ -123,6 +122,7 @@ class BaseUser(_UserTag):
self._public_flags = data.get('public_flags', 0) self._public_flags = data.get('public_flags', 0)
self.bot = data.get('bot', False) self.bot = data.get('bot', False)
self.system = data.get('system', False) self.system = data.get('system', False)
self._avatar_decoration_data = data.get('avatar_decoration_data')
@classmethod @classmethod
def _copy(cls, user: Self) -> Self: def _copy(cls, user: Self) -> Self:
@ -138,6 +138,7 @@ class BaseUser(_UserTag):
self.bot = user.bot self.bot = user.bot
self._state = user._state self._state = user._state
self._public_flags = user._public_flags self._public_flags = user._public_flags
self._avatar_decoration_data = user._avatar_decoration_data
return self return self
@ -187,6 +188,30 @@ class BaseUser(_UserTag):
""" """
return self.avatar or self.default_avatar return self.avatar or self.default_avatar
@property
def avatar_decoration(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns an :class:`Asset` for the avatar decoration the user has.
If the user has not set an avatar decoration, ``None`` is returned.
.. versionadded:: 2.4
"""
if self._avatar_decoration_data is not None:
return Asset._from_avatar_decoration(self._state, self._avatar_decoration_data['asset'])
return None
@property
def avatar_decoration_sku_id(self) -> Optional[int]:
"""Optional[:class:`int`]: Returns the SKU ID of the avatar decoration the user has.
If the user has not set an avatar decoration, ``None`` is returned.
.. versionadded:: 2.4
"""
if self._avatar_decoration_data is not None:
return _get_as_snowflake(self._avatar_decoration_data, 'sku_id')
return None
@property @property
def banner(self) -> Optional[Asset]: def banner(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the user's banner asset, if available. """Optional[:class:`Asset`]: Returns the user's banner asset, if available.

17
discord/voice_state.py

@ -267,26 +267,31 @@ class VoiceConnectionState:
return return
channel_id = int(channel_id)
self.session_id = data['session_id'] self.session_id = data['session_id']
# we got the event while connecting # we got the event while connecting
if self.state in (ConnectionFlowState.set_guild_voice_state, ConnectionFlowState.got_voice_server_update): if self.state in (ConnectionFlowState.set_guild_voice_state, ConnectionFlowState.got_voice_server_update):
if self.state is ConnectionFlowState.set_guild_voice_state: if self.state is ConnectionFlowState.set_guild_voice_state:
self.state = ConnectionFlowState.got_voice_state_update self.state = ConnectionFlowState.got_voice_state_update
# we moved ourselves
if channel_id != self.voice_client.channel.id:
self._update_voice_channel(channel_id)
else: else:
self.state = ConnectionFlowState.got_both_voice_updates self.state = ConnectionFlowState.got_both_voice_updates
return return
if self.state is ConnectionFlowState.connected: if self.state is ConnectionFlowState.connected:
self.voice_client.channel = channel_id and self.guild.get_channel(int(channel_id)) # type: ignore self._update_voice_channel(channel_id)
elif self.state is not ConnectionFlowState.disconnected: elif self.state is not ConnectionFlowState.disconnected:
if channel_id != self.voice_client.channel.id: if channel_id != self.voice_client.channel.id:
# For some unfortunate reason we were moved during the connection flow # For some unfortunate reason we were moved during the connection flow
_log.info('Handling channel move while connecting...') _log.info('Handling channel move while connecting...')
self.voice_client.channel = channel_id and self.guild.get_channel(int(channel_id)) # type: ignore self._update_voice_channel(channel_id)
await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_state_update) await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_state_update)
await self.connect( await self.connect(
reconnect=self.reconnect, reconnect=self.reconnect,
@ -484,6 +489,9 @@ class VoiceConnectionState:
await self.disconnect() await self.disconnect()
return return
if self.voice_client.channel and channel.id == self.voice_client.channel.id:
return
previous_state = self.state previous_state = self.state
# this is only an outgoing ws request # this is only an outgoing ws request
# if it fails, nothing happens and nothing changes (besides self.state) # if it fails, nothing happens and nothing changes (besides self.state)
@ -637,3 +645,6 @@ class VoiceConnectionState:
async def _move_to(self, channel: abc.Snowflake) -> None: async def _move_to(self, channel: abc.Snowflake) -> None:
await self.voice_client.channel.guild.change_voice_state(channel=channel) await self.voice_client.channel.guild.change_voice_state(channel=channel)
self.state = ConnectionFlowState.set_guild_voice_state self.state = ConnectionFlowState.set_guild_voice_state
def _update_voice_channel(self, channel_id: Optional[int]) -> None:
self.voice_client.channel = channel_id and self.guild.get_channel(channel_id) # type: ignore

3
discord/webhook/async_.py

@ -1305,9 +1305,10 @@ class Webhook(BaseWebhook):
'user': { 'user': {
'username': user.name, 'username': user.name,
'discriminator': user.discriminator, 'discriminator': user.discriminator,
'global_name': user.global_name,
'id': user.id, 'id': user.id,
'avatar': user._avatar, 'avatar': user._avatar,
'avatar_decoration_data': user._avatar_decoration_data,
'global_name': user.global_name,
}, },
} }

1
setup.py

@ -103,6 +103,7 @@ setup(
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet', 'Topic :: Internet',
'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',

Loading…
Cancel
Save