Browse Source

Merge branch 'master' of https://github.com/rapptz/discord.py into feat/components-v2

pull/10166/head
DA-344 3 months ago
parent
commit
a93a6391f6
  1. 3
      discord/abc.py
  2. 74
      discord/embeds.py
  3. 2
      discord/ext/commands/context.py
  4. 2
      discord/interactions.py
  5. 35
      discord/message.py
  6. 8
      discord/state.py
  7. 22
      discord/types/embed.py
  8. 18
      docs/api.rst
  9. 11
      docs/faq.rst
  10. 25
      docs/whats_new.rst
  11. 2
      pyproject.toml

3
discord/abc.py

@ -1574,6 +1574,9 @@ class Messageable:
Sending the message failed. Sending the message failed.
~discord.Forbidden ~discord.Forbidden
You do not have the proper permissions to send the message. You do not have the proper permissions to send the message.
~discord.NotFound
You sent a message with the same nonce as one that has been explicitly
deleted shortly earlier.
ValueError ValueError
The ``files`` or ``embeds`` list is not of the appropriate size. The ``files`` or ``embeds`` list is not of the appropriate size.
TypeError TypeError

74
discord/embeds.py

@ -46,7 +46,7 @@ class EmbedProxy:
return len(self.__dict__) return len(self.__dict__)
def __repr__(self) -> str: def __repr__(self) -> str:
inner = ', '.join((f'{k}={v!r}' for k, v in self.__dict__.items() if not k.startswith('_'))) inner = ', '.join((f'{k}={getattr(self, k)!r}' for k in dir(self) if not k.startswith('_')))
return f'EmbedProxy({inner})' return f'EmbedProxy({inner})'
def __getattr__(self, attr: str) -> None: def __getattr__(self, attr: str) -> None:
@ -56,6 +56,16 @@ class EmbedProxy:
return isinstance(other, EmbedProxy) and self.__dict__ == other.__dict__ return isinstance(other, EmbedProxy) and self.__dict__ == other.__dict__
class EmbedMediaProxy(EmbedProxy):
def __init__(self, layer: Dict[str, Any]):
super().__init__(layer)
self._flags = self.__dict__.pop('flags', 0)
@property
def flags(self) -> AttachmentFlags:
return AttachmentFlags._from_value(self._flags or 0)
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
@ -77,12 +87,7 @@ if TYPE_CHECKING:
proxy_url: Optional[str] proxy_url: Optional[str]
height: Optional[int] height: Optional[int]
width: Optional[int] width: Optional[int]
flags: Optional[AttachmentFlags] flags: AttachmentFlags
class _EmbedVideoProxy(Protocol):
url: Optional[str]
height: Optional[int]
width: Optional[int]
class _EmbedProviderProxy(Protocol): class _EmbedProviderProxy(Protocol):
name: Optional[str] name: Optional[str]
@ -148,10 +153,6 @@ class Embed:
colour: Optional[Union[:class:`Colour`, :class:`int`]] colour: Optional[Union[:class:`Colour`, :class:`int`]]
The colour code of the embed. Aliased to ``color`` as well. The colour code of the embed. Aliased to ``color`` as well.
This can be set during initialisation. This can be set during initialisation.
flags: Optional[:class:`EmbedFlags`]
The flags of this embed.
.. versionadded:: 2.5
""" """
__slots__ = ( __slots__ = (
@ -168,7 +169,7 @@ class Embed:
'_author', '_author',
'_fields', '_fields',
'description', 'description',
'flags', '_flags',
) )
def __init__( def __init__(
@ -188,7 +189,7 @@ class Embed:
self.type: EmbedType = type self.type: EmbedType = type
self.url: Optional[str] = url self.url: Optional[str] = url
self.description: Optional[str] = description self.description: Optional[str] = description
self.flags: Optional[EmbedFlags] = None self._flags: int = 0
if self.title is not None: if self.title is not None:
self.title = str(self.title) self.title = str(self.title)
@ -223,6 +224,7 @@ class Embed:
self.type = data.get('type', None) self.type = data.get('type', None)
self.description = data.get('description', None) self.description = data.get('description', None)
self.url = data.get('url', None) self.url = data.get('url', None)
self._flags = data.get('flags', 0)
if self.title is not None: if self.title is not None:
self.title = str(self.title) self.title = str(self.title)
@ -253,11 +255,6 @@ class Embed:
else: else:
setattr(self, '_' + attr, value) setattr(self, '_' + attr, value)
try:
self.flags = EmbedFlags._from_value(data['flags'])
except KeyError:
pass
return self return self
def copy(self) -> Self: def copy(self) -> Self:
@ -318,8 +315,17 @@ class Embed:
and self.image == other.image and self.image == other.image
and self.provider == other.provider and self.provider == other.provider
and self.video == other.video and self.video == other.video
and self._flags == other._flags
) )
@property
def flags(self) -> EmbedFlags:
""":class:`EmbedFlags`: The flags of this embed.
.. versionadded:: 2.5
"""
return EmbedFlags._from_value(self._flags or 0)
@property @property
def colour(self) -> Optional[Colour]: def colour(self) -> Optional[Colour]:
return getattr(self, '_colour', None) return getattr(self, '_colour', None)
@ -408,19 +414,16 @@ class Embed:
Possible attributes you can access are: Possible attributes you can access are:
- ``url`` - ``url`` for the image URL.
- ``proxy_url`` - ``proxy_url`` for the proxied image URL.
- ``width`` - ``width`` for the image width.
- ``height`` - ``height`` for the image height.
- ``flags`` - ``flags`` for the image's attachment flags.
If the attribute has no value then ``None`` is returned. If the attribute has no value then ``None`` is returned.
""" """
# Lying to the type checker for better developer UX. # Lying to the type checker for better developer UX.
data = getattr(self, '_image', {}) return EmbedMediaProxy(getattr(self, '_image', {})) # type: ignore
if 'flags' in data:
data['flags'] = AttachmentFlags._from_value(data['flags'])
return EmbedProxy(data) # type: ignore
def set_image(self, *, url: Optional[Any]) -> Self: def set_image(self, *, url: Optional[Any]) -> Self:
"""Sets the image for the embed content. """Sets the image for the embed content.
@ -454,15 +457,16 @@ class Embed:
Possible attributes you can access are: Possible attributes you can access are:
- ``url`` - ``url`` for the thumbnail URL.
- ``proxy_url`` - ``proxy_url`` for the proxied thumbnail URL.
- ``width`` - ``width`` for the thumbnail width.
- ``height`` - ``height`` for the thumbnail height.
- ``flags`` for the thumbnail's attachment flags.
If the attribute has no value then ``None`` is returned. If the attribute has no value then ``None`` is returned.
""" """
# Lying to the type checker for better developer UX. # Lying to the type checker for better developer UX.
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore return EmbedMediaProxy(getattr(self, '_thumbnail', {})) # type: ignore
def set_thumbnail(self, *, url: Optional[Any]) -> Self: def set_thumbnail(self, *, url: Optional[Any]) -> Self:
"""Sets the thumbnail for the embed content. """Sets the thumbnail for the embed content.
@ -491,19 +495,21 @@ class Embed:
return self return self
@property @property
def video(self) -> _EmbedVideoProxy: def video(self) -> _EmbedMediaProxy:
"""Returns an ``EmbedProxy`` denoting the video contents. """Returns an ``EmbedProxy`` denoting the video contents.
Possible attributes include: Possible attributes include:
- ``url`` for the video URL. - ``url`` for the video URL.
- ``proxy_url`` for the proxied video URL.
- ``height`` for the video height. - ``height`` for the video height.
- ``width`` for the video width. - ``width`` for the video width.
- ``flags`` for the video's attachment flags.
If the attribute has no value then ``None`` is returned. If the attribute has no value then ``None`` is returned.
""" """
# Lying to the type checker for better developer UX. # Lying to the type checker for better developer UX.
return EmbedProxy(getattr(self, '_video', {})) # type: ignore return EmbedMediaProxy(getattr(self, '_video', {})) # type: ignore
@property @property
def provider(self) -> _EmbedProviderProxy: def provider(self) -> _EmbedProviderProxy:

2
discord/ext/commands/context.py

@ -751,7 +751,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
else: else:
return await self.send(content, **kwargs) return await self.send(content, **kwargs)
def typing(self, *, ephemeral: bool = False) -> Union[Typing, DeferTyping]: def typing(self, *, ephemeral: bool = False) -> Union[Typing, DeferTyping[BotT]]:
"""Returns an asynchronous context manager that allows you to send a typing indicator to """Returns an asynchronous context manager that allows you to send a typing indicator to
the destination for an indefinite period of time, or 10 seconds if the context manager the destination for an indefinite period of time, or 10 seconds if the context manager
is called using ``await``. is called using ``await``.

2
discord/interactions.py

@ -727,7 +727,7 @@ class InteractionCallbackResponse(Generic[ClientT]):
activity_instance = resource.get('activity_instance') activity_instance = resource.get('activity_instance')
if message is not None: if message is not None:
self.resource = InteractionMessage( self.resource = InteractionMessage(
state=self._state, state=_InteractionMessageState(self._parent, self._state), # pyright: ignore[reportArgumentType]
channel=self._parent.channel, # type: ignore # channel should be the correct type here channel=self._parent.channel, # type: ignore # channel should be the correct type here
data=message, data=message,
) )

35
discord/message.py

@ -253,11 +253,12 @@ class Attachment(Hashable):
def is_spoiler(self) -> bool: def is_spoiler(self) -> bool:
""":class:`bool`: Whether this attachment contains a spoiler.""" """:class:`bool`: Whether this attachment contains a spoiler."""
return self.filename.startswith('SPOILER_') # The flag is technically always present but no harm to check both
return self.filename.startswith('SPOILER_') or self.flags.spoiler
def is_voice_message(self) -> bool: def is_voice_message(self) -> bool:
""":class:`bool`: Whether this attachment is a voice message.""" """:class:`bool`: Whether this attachment is a voice message."""
return self.duration is not None and 'voice-message' in self.url return self.duration is not None and self.waveform is not None
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<Attachment id={self.id} filename={self.filename!r} url={self.url!r}>' return f'<Attachment id={self.id} filename={self.filename!r} url={self.url!r}>'
@ -609,6 +610,11 @@ class MessageReference:
.. versionadded:: 2.5 .. versionadded:: 2.5
message_id: Optional[:class:`int`] message_id: Optional[:class:`int`]
The id of the message referenced. The id of the message referenced.
This can be ``None`` when this message reference was retrieved from
a system message of one of the following types:
- :attr:`MessageType.channel_follow_add`
- :attr:`MessageType.thread_created`
channel_id: :class:`int` channel_id: :class:`int`
The channel id of the message referenced. The channel id of the message referenced.
guild_id: Optional[:class:`int`] guild_id: Optional[:class:`int`]
@ -2009,9 +2015,16 @@ class Message(PartialMessage, Hashable):
The :class:`TextChannel` or :class:`Thread` that the message was sent from. The :class:`TextChannel` or :class:`Thread` that the message was sent from.
Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message.
reference: Optional[:class:`~discord.MessageReference`] reference: Optional[:class:`~discord.MessageReference`]
The message that this message references. This is only applicable to messages of The message that this message references. This is only applicable to
type :attr:`MessageType.pins_add`, crossposted messages created by a message replies (:attr:`MessageType.reply`), crossposted messages created by
followed channel integration, or message replies. a followed channel integration, forwarded messages, and messages of type:
- :attr:`MessageType.pins_add`
- :attr:`MessageType.channel_follow_add`
- :attr:`MessageType.thread_created`
- :attr:`MessageType.thread_starter_message`
- :attr:`MessageType.poll_result`
- :attr:`MessageType.context_menu_command`
.. versionadded:: 1.5 .. versionadded:: 1.5
@ -2181,15 +2194,15 @@ class Message(PartialMessage, Hashable):
self._state: ConnectionState = state self._state: ConnectionState = state
self.webhook_id: Optional[int] = utils._get_as_snowflake(data, 'webhook_id') self.webhook_id: Optional[int] = utils._get_as_snowflake(data, 'webhook_id')
self.reactions: List[Reaction] = [Reaction(message=self, data=d) for d in data.get('reactions', [])] self.reactions: List[Reaction] = [Reaction(message=self, data=d) for d in data.get('reactions', [])]
self.attachments: List[Attachment] = [Attachment(data=a, state=self._state) for a in data['attachments']] self.attachments: List[Attachment] = [Attachment(data=a, state=self._state) for a in data.get('attachments', [])]
self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']] self.embeds: List[Embed] = [Embed.from_dict(a) for a in data.get('embeds', [])]
self.activity: Optional[MessageActivityPayload] = data.get('activity') self.activity: Optional[MessageActivityPayload] = data.get('activity')
self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp']) self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('edited_timestamp'))
self.type: MessageType = try_enum(MessageType, data['type']) self.type: MessageType = try_enum(MessageType, data['type'])
self.pinned: bool = data['pinned'] self.pinned: bool = data.get('pinned', False)
self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0)) self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0))
self.mention_everyone: bool = data['mention_everyone'] self.mention_everyone: bool = data.get('mention_everyone', False)
self.tts: bool = data['tts'] self.tts: bool = data.get('tts', False)
self.content: str = data['content'] self.content: str = data['content']
self.nonce: Optional[Union[int, str]] = data.get('nonce') self.nonce: Optional[Union[int, str]] = data.get('nonce')
self.position: Optional[int] = data.get('position') self.position: Optional[int] = data.get('position')

8
discord/state.py

@ -1553,12 +1553,8 @@ class ConnectionState(Generic[ClientT]):
def parse_guild_scheduled_event_delete(self, data: gw.GuildScheduledEventDeleteEvent) -> None: def parse_guild_scheduled_event_delete(self, data: gw.GuildScheduledEventDeleteEvent) -> None:
guild = self._get_guild(int(data['guild_id'])) guild = self._get_guild(int(data['guild_id']))
if guild is not None: if guild is not None:
try: scheduled_event = guild._scheduled_events.pop(int(data['id']), ScheduledEvent(state=self, data=data))
scheduled_event = guild._scheduled_events.pop(int(data['id'])) self.dispatch('scheduled_event_delete', scheduled_event)
except KeyError:
pass
else:
self.dispatch('scheduled_event_delete', scheduled_event)
else: else:
_log.debug('SCHEDULED_EVENT_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id']) _log.debug('SCHEDULED_EVENT_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id'])

22
discord/types/embed.py

@ -38,28 +38,14 @@ class EmbedField(TypedDict):
inline: NotRequired[bool] inline: NotRequired[bool]
class EmbedThumbnail(TypedDict, total=False): class EmbedMedia(TypedDict, total=False):
url: Required[str] url: Required[str]
proxy_url: str proxy_url: str
height: int height: int
width: int width: int
class EmbedVideo(TypedDict, total=False):
url: str
proxy_url: str
height: int
width: int
flags: int flags: int
class EmbedImage(TypedDict, total=False):
url: Required[str]
proxy_url: str
height: int
width: int
class EmbedProvider(TypedDict, total=False): class EmbedProvider(TypedDict, total=False):
name: str name: str
url: str url: str
@ -83,9 +69,9 @@ class Embed(TypedDict, total=False):
timestamp: str timestamp: str
color: int color: int
footer: EmbedFooter footer: EmbedFooter
image: EmbedImage image: EmbedMedia
thumbnail: EmbedThumbnail thumbnail: EmbedMedia
video: EmbedVideo video: EmbedMedia
provider: EmbedProvider provider: EmbedProvider
author: EmbedAuthor author: EmbedAuthor
fields: List[EmbedField] fields: List[EmbedField]

18
docs/api.rst

@ -2418,7 +2418,7 @@ of :class:`enum.Enum`.
When this is the action, the type of :attr:`~AuditLogEntry.extra` is When this is the action, the type of :attr:`~AuditLogEntry.extra` is
set to an unspecified proxy object with two attributes: set to an unspecified proxy object with two attributes:
- ``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the members were moved. - ``channel``: An :class:`abc.Connectable` or :class:`Object` with the channel ID where the members were moved.
- ``count``: An integer specifying how many members were moved. - ``count``: An integer specifying how many members were moved.
.. versionadded:: 1.3 .. versionadded:: 1.3
@ -3851,17 +3851,25 @@ of :class:`enum.Enum`.
.. versionadded:: 2.5 .. versionadded:: 2.5
.. attribute:: reply .. attribute:: default
A standard reference used by message replies (:attr:`MessageType.reply`),
crossposted messaged created by a followed channel integration, and messages of type:
A message reply. - :attr:`MessageType.pins_add`
- :attr:`MessageType.channel_follow_add`
- :attr:`MessageType.thread_created`
- :attr:`MessageType.thread_starter_message`
- :attr:`MessageType.poll_result`
- :attr:`MessageType.context_menu_command`
.. attribute:: forward .. attribute:: forward
A forwarded message. A forwarded message.
.. attribute:: default .. attribute:: reply
An alias for :attr:`.reply`. An alias for :attr:`.default`.
.. class:: MediaItemLoadingState .. class:: MediaItemLoadingState

11
docs/faq.rst

@ -439,7 +439,7 @@ How can I disable all items on timeout?
This requires three steps. This requires three steps.
1. Attach a message to the :class:`~discord.ui.View` using either the return type of :meth:`~abc.Messageable.send` or retrieving it via :meth:`Interaction.original_response`. 1. Attach a message to the :class:`~discord.ui.View` using either the return type of :meth:`~abc.Messageable.send` or retrieving it via :attr:`InteractionCallbackResponse.resource`.
2. Inside :meth:`~ui.View.on_timeout`, loop over all items inside the view and mark them disabled. 2. Inside :meth:`~ui.View.on_timeout`, loop over all items inside the view and mark them disabled.
3. Edit the message we retrieved in step 1 with the newly modified view. 3. Edit the message we retrieved in step 1 with the newly modified view.
@ -467,7 +467,7 @@ Putting it all together, we can do this in a text command:
# Step 1 # Step 1
view.message = await ctx.send('Press me!', view=view) view.message = await ctx.send('Press me!', view=view)
Application commands do not return a message when you respond with :meth:`InteractionResponse.send_message`, therefore in order to reliably do this we should retrieve the message using :meth:`Interaction.original_response`. Application commands, when you respond with :meth:`InteractionResponse.send_message`, return an instance of :class:`InteractionCallbackResponse` which contains the message you sent. This is the message you should attach to the view.
Putting it all together, using the previous view definition: Putting it all together, using the previous view definition:
@ -477,10 +477,13 @@ Putting it all together, using the previous view definition:
async def more_timeout_example(interaction): async def more_timeout_example(interaction):
"""Another example to showcase disabling buttons on timing out""" """Another example to showcase disabling buttons on timing out"""
view = MyView() view = MyView()
await interaction.response.send_message('Press me!', view=view) callback = await interaction.response.send_message('Press me!', view=view)
# Step 1 # Step 1
view.message = await interaction.original_response() resource = callback.resource
# making sure it's an interaction response message
if isinstance(resource, discord.InteractionMessage):
view.message = resource
Application Commands Application Commands

25
docs/whats_new.rst

@ -11,6 +11,30 @@ Changelog
This page keeps a detailed human friendly rendering of what's new and changed This page keeps a detailed human friendly rendering of what's new and changed
in specific versions. in specific versions.
.. _vp2p5p2:
v2.5.2
-------
Bug Fixes
~~~~~~~~~~
- Fix a serialization issue when sending embeds (:issue:`10126`)
.. _vp2p5p1:
v2.5.1
-------
Bug Fixes
~~~~~~~~~~
- Fix :attr:`InteractionCallbackResponse.resource` having incorrect state (:issue:`10107`)
- Create :class:`ScheduledEvent` on cache miss for :func:`on_scheduled_event_delete` (:issue:`10113`)
- Add defaults for :class:`Message` creation preventing some crashes (:issue:`10115`)
- Fix :meth:`Attachment.is_spoiler` and :meth:`Attachment.is_voice_message` being incorrect (:issue:`10122`)
.. _vp2p5p0: .. _vp2p5p0:
v2.5.0 v2.5.0
@ -63,7 +87,6 @@ New Features
- Add :attr:`PartialWebhookChannel.mention` attribute (:issue:`10101`) - Add :attr:`PartialWebhookChannel.mention` attribute (:issue:`10101`)
- Add support for sending stateless views for :class:`SyncWebhook` or webhooks with no state (:issue:`10089`) - Add support for sending stateless views for :class:`SyncWebhook` or webhooks with no state (:issue:`10089`)
- Add
- Add richer :meth:`Role.move` interface (:issue:`10100`) - Add richer :meth:`Role.move` interface (:issue:`10100`)
- Add support for :class:`EmbedFlags` via :attr:`Embed.flags` (:issue:`10085`) - Add support for :class:`EmbedFlags` via :attr:`Embed.flags` (:issue:`10085`)
- Add new flags for :class:`AttachmentFlags` (:issue:`10085`) - Add new flags for :class:`AttachmentFlags` (:issue:`10085`)

2
pyproject.toml

@ -36,7 +36,7 @@ Documentation = "https://discordpy.readthedocs.io/en/latest/"
dependencies = { file = "requirements.txt" } dependencies = { file = "requirements.txt" }
[project.optional-dependencies] [project.optional-dependencies]
voice = ["PyNaCl>=1.3.0,<1.6"] voice = ["PyNaCl>=1.5.0,<1.6"]
docs = [ docs = [
"sphinx==4.4.0", "sphinx==4.4.0",
"sphinxcontrib_trio==1.1.2", "sphinxcontrib_trio==1.1.2",

Loading…
Cancel
Save