Browse Source

Add support for message forwarding

Co-authored-by: Red Magnos <redmagnos@gmail.com>
Co-authored-by: MCausc78 <mcausc78@gmail.com>
Co-authored-by: owocado <24418520+owocado@users.noreply.github.com>
Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com>
pull/9970/head
DA344 6 months ago
committed by GitHub
parent
commit
99a7093c34
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      discord/enums.py
  2. 223
      discord/message.py
  3. 18
      discord/types/message.py
  4. 26
      docs/api.rst

6
discord/enums.py

@ -220,6 +220,12 @@ class ChannelType(Enum):
return self.name return self.name
class MessageReferenceType(Enum):
default = 0
reply = 0
forward = 1
class MessageType(Enum): class MessageType(Enum):
default = 0 default = 0
recipient_add = 1 recipient_add = 1

223
discord/message.py

@ -32,6 +32,7 @@ from os import PathLike
from typing import ( from typing import (
Dict, Dict,
TYPE_CHECKING, TYPE_CHECKING,
Literal,
Sequence, Sequence,
Union, Union,
List, List,
@ -49,7 +50,7 @@ from .asset import Asset
from .reaction import Reaction from .reaction import Reaction
from .emoji import Emoji from .emoji import Emoji
from .partial_emoji import PartialEmoji from .partial_emoji import PartialEmoji
from .enums import InteractionType, MessageType, ChannelType, try_enum from .enums import InteractionType, MessageReferenceType, MessageType, ChannelType, try_enum
from .errors import HTTPException from .errors import HTTPException
from .components import _component_factory from .components import _component_factory
from .embeds import Embed from .embeds import Embed
@ -72,6 +73,7 @@ if TYPE_CHECKING:
Message as MessagePayload, Message as MessagePayload,
Attachment as AttachmentPayload, Attachment as AttachmentPayload,
MessageReference as MessageReferencePayload, MessageReference as MessageReferencePayload,
MessageSnapshot as MessageSnapshotPayload,
MessageApplication as MessageApplicationPayload, MessageApplication as MessageApplicationPayload,
MessageActivity as MessageActivityPayload, MessageActivity as MessageActivityPayload,
RoleSubscriptionData as RoleSubscriptionDataPayload, RoleSubscriptionData as RoleSubscriptionDataPayload,
@ -111,6 +113,7 @@ __all__ = (
'PartialMessage', 'PartialMessage',
'MessageInteraction', 'MessageInteraction',
'MessageReference', 'MessageReference',
'MessageSnapshot',
'DeletedReferencedMessage', 'DeletedReferencedMessage',
'MessageApplication', 'MessageApplication',
'RoleSubscriptionInfo', 'RoleSubscriptionInfo',
@ -464,6 +467,133 @@ class DeletedReferencedMessage:
return self._parent.guild_id return self._parent.guild_id
class MessageSnapshot:
"""Represents a message snapshot attached to a forwarded message.
.. versionadded:: 2.5
Attributes
-----------
type: :class:`MessageType`
The type of the forwarded message.
content: :class:`str`
The actual contents of the forwarded message.
embeds: List[:class:`Embed`]
A list of embeds the forwarded message has.
attachments: List[:class:`Attachment`]
A list of attachments given to the forwarded message.
created_at: :class:`datetime.datetime`
The forwarded message's time of creation.
flags: :class:`MessageFlags`
Extra features of the the message snapshot.
stickers: List[:class:`StickerItem`]
A list of sticker items given to the message.
components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`]]
A list of components in the message.
"""
__slots__ = (
'_cs_raw_channel_mentions',
'_cs_cached_message',
'_cs_raw_mentions',
'_cs_raw_role_mentions',
'_edited_timestamp',
'attachments',
'content',
'embeds',
'flags',
'created_at',
'type',
'stickers',
'components',
'_state',
)
@classmethod
def _from_value(
cls,
state: ConnectionState,
message_snapshots: Optional[List[Dict[Literal['message'], MessageSnapshotPayload]]],
) -> List[Self]:
if not message_snapshots:
return []
return [cls(state, snapshot['message']) for snapshot in message_snapshots]
def __init__(self, state: ConnectionState, data: MessageSnapshotPayload):
self.type: MessageType = try_enum(MessageType, data['type'])
self.content: str = data['content']
self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']]
self.attachments: List[Attachment] = [Attachment(data=a, state=state) for a in data['attachments']]
self.created_at: datetime.datetime = utils.parse_time(data['timestamp'])
self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp'])
self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0))
self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('stickers_items', [])]
self.components: List[MessageComponentType] = []
for component_data in data.get('components', []):
component = _component_factory(component_data)
if component is not None:
self.components.append(component)
self._state: ConnectionState = state
def __repr__(self) -> str:
name = self.__class__.__name__
return f'<{name} type={self.type!r} created_at={self.created_at!r} flags={self.flags!r}>'
@utils.cached_slot_property('_cs_raw_mentions')
def raw_mentions(self) -> List[int]:
"""List[:class:`int`]: A property that returns an array of user IDs matched with
the syntax of ``<@user_id>`` in the message content.
This allows you to receive the user IDs of mentioned users
even in a private message context.
"""
return [int(x) for x in re.findall(r'<@!?([0-9]{15,20})>', self.content)]
@utils.cached_slot_property('_cs_raw_channel_mentions')
def raw_channel_mentions(self) -> List[int]:
"""List[:class:`int`]: A property that returns an array of channel IDs matched with
the syntax of ``<#channel_id>`` in the message content.
"""
return [int(x) for x in re.findall(r'<#([0-9]{15,20})>', self.content)]
@utils.cached_slot_property('_cs_raw_role_mentions')
def raw_role_mentions(self) -> List[int]:
"""List[:class:`int`]: A property that returns an array of role IDs matched with
the syntax of ``<@&role_id>`` in the message content.
"""
return [int(x) for x in re.findall(r'<@&([0-9]{15,20})>', self.content)]
@utils.cached_slot_property('_cs_cached_message')
def cached_message(self) -> Optional[Message]:
"""Optional[:class:`Message`]: Returns the cached message this snapshot points to, if any."""
state = self._state
return (
utils.find(
lambda m: (
m.created_at == self.created_at
and m.edited_at == self.edited_at
and m.content == self.content
and m.embeds == self.embeds
and m.components == self.components
and m.stickers == self.stickers
and m.attachments == self.attachments
and m.flags == self.flags
),
reversed(state._messages),
)
if state._messages
else None
)
@property
def edited_at(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the edited time of the forwarded message."""
return self._edited_timestamp
class MessageReference: class MessageReference:
"""Represents a reference to a :class:`~discord.Message`. """Represents a reference to a :class:`~discord.Message`.
@ -474,6 +604,10 @@ class MessageReference:
Attributes Attributes
----------- -----------
type: :class:`MessageReferenceType`
The type of message reference.
.. 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.
channel_id: :class:`int` channel_id: :class:`int`
@ -498,10 +632,19 @@ class MessageReference:
.. versionadded:: 1.6 .. versionadded:: 1.6
""" """
__slots__ = ('message_id', 'channel_id', 'guild_id', 'fail_if_not_exists', 'resolved', '_state') __slots__ = ('type', 'message_id', 'channel_id', 'guild_id', 'fail_if_not_exists', 'resolved', '_state')
def __init__(self, *, message_id: int, channel_id: int, guild_id: Optional[int] = None, fail_if_not_exists: bool = True): def __init__(
self,
*,
message_id: int,
channel_id: int,
guild_id: Optional[int] = None,
fail_if_not_exists: bool = True,
type: MessageReferenceType = MessageReferenceType.reply,
):
self._state: Optional[ConnectionState] = None self._state: Optional[ConnectionState] = None
self.type: MessageReferenceType = type
self.resolved: Optional[Union[Message, DeletedReferencedMessage]] = None self.resolved: Optional[Union[Message, DeletedReferencedMessage]] = None
self.message_id: Optional[int] = message_id self.message_id: Optional[int] = message_id
self.channel_id: int = channel_id self.channel_id: int = channel_id
@ -511,6 +654,7 @@ class MessageReference:
@classmethod @classmethod
def with_state(cls, state: ConnectionState, data: MessageReferencePayload) -> Self: def with_state(cls, state: ConnectionState, data: MessageReferencePayload) -> Self:
self = cls.__new__(cls) self = cls.__new__(cls)
self.type = try_enum(MessageReferenceType, data.get('type', 0))
self.message_id = utils._get_as_snowflake(data, 'message_id') self.message_id = utils._get_as_snowflake(data, 'message_id')
self.channel_id = int(data['channel_id']) self.channel_id = int(data['channel_id'])
self.guild_id = utils._get_as_snowflake(data, 'guild_id') self.guild_id = utils._get_as_snowflake(data, 'guild_id')
@ -520,7 +664,13 @@ class MessageReference:
return self return self
@classmethod @classmethod
def from_message(cls, message: PartialMessage, *, fail_if_not_exists: bool = True) -> Self: def from_message(
cls,
message: PartialMessage,
*,
fail_if_not_exists: bool = True,
type: MessageReferenceType = MessageReferenceType.reply,
) -> Self:
"""Creates a :class:`MessageReference` from an existing :class:`~discord.Message`. """Creates a :class:`MessageReference` from an existing :class:`~discord.Message`.
.. versionadded:: 1.6 .. versionadded:: 1.6
@ -534,6 +684,10 @@ class MessageReference:
if the message no longer exists or Discord could not fetch the message. if the message no longer exists or Discord could not fetch the message.
.. versionadded:: 1.7 .. versionadded:: 1.7
type: :class:`~discord.MessageReferenceType`
The type of message reference this is.
.. versionadded:: 2.5
Returns Returns
------- -------
@ -545,6 +699,7 @@ class MessageReference:
channel_id=message.channel.id, channel_id=message.channel.id,
guild_id=getattr(message.guild, 'id', None), guild_id=getattr(message.guild, 'id', None),
fail_if_not_exists=fail_if_not_exists, fail_if_not_exists=fail_if_not_exists,
type=type,
) )
self._state = message._state self._state = message._state
return self return self
@ -567,7 +722,9 @@ class MessageReference:
return f'<MessageReference message_id={self.message_id!r} channel_id={self.channel_id!r} guild_id={self.guild_id!r}>' return f'<MessageReference message_id={self.message_id!r} channel_id={self.channel_id!r} guild_id={self.guild_id!r}>'
def to_dict(self) -> MessageReferencePayload: def to_dict(self) -> MessageReferencePayload:
result: Dict[str, Any] = {'message_id': self.message_id} if self.message_id is not None else {} result: Dict[str, Any] = (
{'type': self.type.value, 'message_id': self.message_id} if self.message_id is not None else {}
)
result['channel_id'] = self.channel_id result['channel_id'] = self.channel_id
if self.guild_id is not None: if self.guild_id is not None:
result['guild_id'] = self.guild_id result['guild_id'] = self.guild_id
@ -1699,7 +1856,12 @@ class PartialMessage(Hashable):
return Message(state=self._state, channel=self.channel, data=data) return Message(state=self._state, channel=self.channel, data=data)
def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: def to_reference(
self,
*,
fail_if_not_exists: bool = True,
type: MessageReferenceType = MessageReferenceType.reply,
) -> MessageReference:
"""Creates a :class:`~discord.MessageReference` from the current message. """Creates a :class:`~discord.MessageReference` from the current message.
.. versionadded:: 1.6 .. versionadded:: 1.6
@ -1711,6 +1873,10 @@ class PartialMessage(Hashable):
if the message no longer exists or Discord could not fetch the message. if the message no longer exists or Discord could not fetch the message.
.. versionadded:: 1.7 .. versionadded:: 1.7
type: :class:`MessageReferenceType`
The type of message reference.
.. versionadded:: 2.5
Returns Returns
--------- ---------
@ -1718,7 +1884,44 @@ class PartialMessage(Hashable):
The reference to this message. The reference to this message.
""" """
return MessageReference.from_message(self, fail_if_not_exists=fail_if_not_exists) return MessageReference.from_message(self, fail_if_not_exists=fail_if_not_exists, type=type)
async def forward(
self,
destination: MessageableChannel,
*,
fail_if_not_exists: bool = True,
) -> Message:
"""|coro|
Forwards this message to a channel.
.. versionadded:: 2.5
Parameters
----------
destination: :class:`~discord.abc.Messageable`
The channel to forward this message to.
fail_if_not_exists: :class:`bool`
Whether replying using the message reference should raise :class:`HTTPException`
if the message no longer exists or Discord could not fetch the message.
Raises
------
~discord.HTTPException
Forwarding the message failed.
Returns
-------
:class:`.Message`
The message sent to the channel.
"""
reference = self.to_reference(
fail_if_not_exists=fail_if_not_exists,
type=MessageReferenceType.forward,
)
ret = await destination.send(reference=reference)
return ret
def to_message_reference_dict(self) -> MessageReferencePayload: def to_message_reference_dict(self) -> MessageReferencePayload:
data: MessageReferencePayload = { data: MessageReferencePayload = {
@ -1881,6 +2084,10 @@ class Message(PartialMessage, Hashable):
purchase_notification: Optional[:class:`PurchaseNotification`] purchase_notification: Optional[:class:`PurchaseNotification`]
The data of the purchase notification that prompted this :attr:`MessageType.purchase_notification` message. The data of the purchase notification that prompted this :attr:`MessageType.purchase_notification` message.
.. versionadded:: 2.5
message_snapshots: List[:class:`MessageSnapshot`]
The message snapshots attached to this message.
.. versionadded:: 2.5 .. versionadded:: 2.5
""" """
@ -1920,6 +2127,7 @@ class Message(PartialMessage, Hashable):
'poll', 'poll',
'call', 'call',
'purchase_notification', 'purchase_notification',
'message_snapshots',
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@ -1958,6 +2166,7 @@ class Message(PartialMessage, Hashable):
self.position: Optional[int] = data.get('position') self.position: Optional[int] = data.get('position')
self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id') self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id')
self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])]
self.message_snapshots: List[MessageSnapshot] = MessageSnapshot._from_value(state, data.get('message_snapshots'))
self.poll: Optional[Poll] = None self.poll: Optional[Poll] = None
try: try:

18
discord/types/message.py

@ -102,7 +102,11 @@ class MessageApplication(TypedDict):
cover_image: NotRequired[str] cover_image: NotRequired[str]
MessageReferenceType = Literal[0, 1]
class MessageReference(TypedDict, total=False): class MessageReference(TypedDict, total=False):
type: MessageReferenceType
message_id: Snowflake message_id: Snowflake
channel_id: Required[Snowflake] channel_id: Required[Snowflake]
guild_id: Snowflake guild_id: Snowflake
@ -173,6 +177,20 @@ MessageType = Literal[
] ]
class MessageSnapshot(TypedDict):
type: MessageType
content: str
embeds: List[Embed]
attachments: List[Attachment]
timestamp: str
edited_timestamp: Optional[str]
flags: NotRequired[int]
mentions: List[UserWithMember]
mention_roles: SnowflakeList
stickers_items: NotRequired[List[StickerItem]]
components: NotRequired[List[Component]]
class Message(PartialMessage): class Message(PartialMessage):
id: Snowflake id: Snowflake
author: User author: User

26
docs/api.rst

@ -3810,6 +3810,24 @@ of :class:`enum.Enum`.
The subscription is inactive and not being charged. The subscription is inactive and not being charged.
.. class:: MessageReferenceType
Represents the type of a message reference.
.. versionadded:: 2.5
.. attribute:: reply
A message reply.
.. attribute:: forward
A forwarded message.
.. attribute:: default
An alias for :attr:`.reply`.
.. _discord-api-audit-logs: .. _discord-api-audit-logs:
Audit Log Data Audit Log Data
@ -5353,6 +5371,14 @@ PollAnswer
.. _discord_api_data: .. _discord_api_data:
MessageSnapshot
~~~~~~~~~~~~~~~~~
.. attributetable:: MessageSnapshot
.. autoclass:: MessageSnapshot
:members:
Data Classes Data Classes
-------------- --------------

Loading…
Cancel
Save