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.
~discord.Forbidden
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
The ``files`` or ``embeds`` list is not of the appropriate size.
TypeError

74
discord/embeds.py

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

2
discord/ext/commands/context.py

@ -751,7 +751,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
else:
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
the destination for an indefinite period of time, or 10 seconds if the context manager
is called using ``await``.

2
discord/interactions.py

@ -727,7 +727,7 @@ class InteractionCallbackResponse(Generic[ClientT]):
activity_instance = resource.get('activity_instance')
if message is not None:
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
data=message,
)

35
discord/message.py

@ -253,11 +253,12 @@ class Attachment(Hashable):
def is_spoiler(self) -> bool:
""":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:
""":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:
return f'<Attachment id={self.id} filename={self.filename!r} url={self.url!r}>'
@ -609,6 +610,11 @@ class MessageReference:
.. versionadded:: 2.5
message_id: Optional[:class:`int`]
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`
The channel id of the message referenced.
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.
Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message.
reference: Optional[:class:`~discord.MessageReference`]
The message that this message references. This is only applicable to messages of
type :attr:`MessageType.pins_add`, crossposted messages created by a
followed channel integration, or message replies.
The message that this message references. This is only applicable to
message replies (:attr:`MessageType.reply`), crossposted messages created by
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
@ -2181,15 +2194,15 @@ class Message(PartialMessage, Hashable):
self._state: ConnectionState = state
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.attachments: List[Attachment] = [Attachment(data=a, state=self._state) for a in data['attachments']]
self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']]
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.get('embeds', [])]
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.pinned: bool = data['pinned']
self.pinned: bool = data.get('pinned', False)
self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0))
self.mention_everyone: bool = data['mention_everyone']
self.tts: bool = data['tts']
self.mention_everyone: bool = data.get('mention_everyone', False)
self.tts: bool = data.get('tts', False)
self.content: str = data['content']
self.nonce: Optional[Union[int, str]] = data.get('nonce')
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:
guild = self._get_guild(int(data['guild_id']))
if guild is not None:
try:
scheduled_event = guild._scheduled_events.pop(int(data['id']))
except KeyError:
pass
else:
self.dispatch('scheduled_event_delete', scheduled_event)
scheduled_event = guild._scheduled_events.pop(int(data['id']), ScheduledEvent(state=self, data=data))
self.dispatch('scheduled_event_delete', scheduled_event)
else:
_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]
class EmbedThumbnail(TypedDict, total=False):
class EmbedMedia(TypedDict, total=False):
url: Required[str]
proxy_url: str
height: int
width: int
class EmbedVideo(TypedDict, total=False):
url: str
proxy_url: str
height: int
width: int
flags: int
class EmbedImage(TypedDict, total=False):
url: Required[str]
proxy_url: str
height: int
width: int
class EmbedProvider(TypedDict, total=False):
name: str
url: str
@ -83,9 +69,9 @@ class Embed(TypedDict, total=False):
timestamp: str
color: int
footer: EmbedFooter
image: EmbedImage
thumbnail: EmbedThumbnail
video: EmbedVideo
image: EmbedMedia
thumbnail: EmbedMedia
video: EmbedMedia
provider: EmbedProvider
author: EmbedAuthor
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
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.
.. versionadded:: 1.3
@ -3851,17 +3851,25 @@ of :class:`enum.Enum`.
.. 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
A forwarded message.
.. attribute:: default
.. attribute:: reply
An alias for :attr:`.reply`.
An alias for :attr:`.default`.
.. class:: MediaItemLoadingState

11
docs/faq.rst

@ -439,7 +439,7 @@ How can I disable all items on timeout?
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.
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
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:
@ -477,10 +477,13 @@ Putting it all together, using the previous view definition:
async def more_timeout_example(interaction):
"""Another example to showcase disabling buttons on timing out"""
view = MyView()
await interaction.response.send_message('Press me!', view=view)
callback = await interaction.response.send_message('Press me!', view=view)
# 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

25
docs/whats_new.rst

@ -11,6 +11,30 @@ Changelog
This page keeps a detailed human friendly rendering of what's new and changed
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:
v2.5.0
@ -63,7 +87,6 @@ New Features
- Add :attr:`PartialWebhookChannel.mention` attribute (:issue:`10101`)
- 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 support for :class:`EmbedFlags` via :attr:`Embed.flags` (: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" }
[project.optional-dependencies]
voice = ["PyNaCl>=1.3.0,<1.6"]
voice = ["PyNaCl>=1.5.0,<1.6"]
docs = [
"sphinx==4.4.0",
"sphinxcontrib_trio==1.1.2",

Loading…
Cancel
Save