Browse Source

Add support for message forwarding

Co-authored-by: Red Magnos <[email protected]>
Co-authored-by: MCausc78 <[email protected]>
Co-authored-by: owocado <[email protected]>
Co-authored-by: Danny <[email protected]>
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
class MessageReferenceType(Enum):
default = 0
reply = 0
forward = 1
class MessageType(Enum):
default = 0
recipient_add = 1

223
discord/message.py

@ -32,6 +32,7 @@ from os import PathLike
from typing import (
Dict,
TYPE_CHECKING,
Literal,
Sequence,
Union,
List,
@ -49,7 +50,7 @@ from .asset import Asset
from .reaction import Reaction
from .emoji import Emoji
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 .components import _component_factory
from .embeds import Embed
@ -72,6 +73,7 @@ if TYPE_CHECKING:
Message as MessagePayload,
Attachment as AttachmentPayload,
MessageReference as MessageReferencePayload,
MessageSnapshot as MessageSnapshotPayload,
MessageApplication as MessageApplicationPayload,
MessageActivity as MessageActivityPayload,
RoleSubscriptionData as RoleSubscriptionDataPayload,
@ -111,6 +113,7 @@ __all__ = (
'PartialMessage',
'MessageInteraction',
'MessageReference',
'MessageSnapshot',
'DeletedReferencedMessage',
'MessageApplication',
'RoleSubscriptionInfo',
@ -464,6 +467,133 @@ class DeletedReferencedMessage:
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:
"""Represents a reference to a :class:`~discord.Message`.
@ -474,6 +604,10 @@ class MessageReference:
Attributes
-----------
type: :class:`MessageReferenceType`
The type of message reference.
.. versionadded:: 2.5
message_id: Optional[:class:`int`]
The id of the message referenced.
channel_id: :class:`int`
@ -498,10 +632,19 @@ class MessageReference:
.. 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.type: MessageReferenceType = type
self.resolved: Optional[Union[Message, DeletedReferencedMessage]] = None
self.message_id: Optional[int] = message_id
self.channel_id: int = channel_id
@ -511,6 +654,7 @@ class MessageReference:
@classmethod
def with_state(cls, state: ConnectionState, data: MessageReferencePayload) -> Self:
self = cls.__new__(cls)
self.type = try_enum(MessageReferenceType, data.get('type', 0))
self.message_id = utils._get_as_snowflake(data, 'message_id')
self.channel_id = int(data['channel_id'])
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
@ -520,7 +664,13 @@ class MessageReference:
return self
@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`.
.. versionadded:: 1.6
@ -534,6 +684,10 @@ class MessageReference:
if the message no longer exists or Discord could not fetch the message.
.. versionadded:: 1.7
type: :class:`~discord.MessageReferenceType`
The type of message reference this is.
.. versionadded:: 2.5
Returns
-------
@ -545,6 +699,7 @@ class MessageReference:
channel_id=message.channel.id,
guild_id=getattr(message.guild, 'id', None),
fail_if_not_exists=fail_if_not_exists,
type=type,
)
self._state = message._state
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}>'
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
if self.guild_id is not None:
result['guild_id'] = self.guild_id
@ -1699,7 +1856,12 @@ class PartialMessage(Hashable):
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.
.. versionadded:: 1.6
@ -1711,6 +1873,10 @@ class PartialMessage(Hashable):
if the message no longer exists or Discord could not fetch the message.
.. versionadded:: 1.7
type: :class:`MessageReferenceType`
The type of message reference.
.. versionadded:: 2.5
Returns
---------
@ -1718,7 +1884,44 @@ class PartialMessage(Hashable):
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:
data: MessageReferencePayload = {
@ -1881,6 +2084,10 @@ class Message(PartialMessage, Hashable):
purchase_notification: Optional[:class:`PurchaseNotification`]
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
"""
@ -1920,6 +2127,7 @@ class Message(PartialMessage, Hashable):
'poll',
'call',
'purchase_notification',
'message_snapshots',
)
if TYPE_CHECKING:
@ -1958,6 +2166,7 @@ class Message(PartialMessage, Hashable):
self.position: Optional[int] = data.get('position')
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.message_snapshots: List[MessageSnapshot] = MessageSnapshot._from_value(state, data.get('message_snapshots'))
self.poll: Optional[Poll] = None
try:

18
discord/types/message.py

@ -102,7 +102,11 @@ class MessageApplication(TypedDict):
cover_image: NotRequired[str]
MessageReferenceType = Literal[0, 1]
class MessageReference(TypedDict, total=False):
type: MessageReferenceType
message_id: Snowflake
channel_id: Required[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):
id: Snowflake
author: User

26
docs/api.rst

@ -3810,6 +3810,24 @@ of :class:`enum.Enum`.
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:
Audit Log Data
@ -5353,6 +5371,14 @@ PollAnswer
.. _discord_api_data:
MessageSnapshot
~~~~~~~~~~~~~~~~~
.. attributetable:: MessageSnapshot
.. autoclass:: MessageSnapshot
:members:
Data Classes
--------------

Loading…
Cancel
Save