Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Message Forwarding #9950

Merged
merged 41 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
6f9f5d6
Added warning for my fork :v
DA-344 Mar 20, 2024
6561f74
Create message reference type
redmvc Jul 19, 2024
43e0fd2
Create MessageSnapshot class
redmvc Jul 19, 2024
0c6eb9e
Store list of message snapshots in message
redmvc Jul 19, 2024
7b7dd83
Add MessageSnapshot to exported classes
redmvc Jul 19, 2024
cc10a75
Remove trailing spaces
redmvc Jul 22, 2024
4c65968
Add MessageSnapshit to api.rst
redmvc Jul 22, 2024
57f5949
Typo fix
redmvc Jul 22, 2024
ea5763b
Remove incorrect documentation
redmvc Jul 22, 2024
6ed45c1
Add MessageSnapshot to api.rst
redmvc Jul 22, 2024
46d157c
Typo fix
redmvc Jul 22, 2024
aa01eca
Remove incorrect documentation
redmvc Jul 22, 2024
6a7933c
Merge branch 'feature/message_snapshots' of https://github.com/redmvc…
redmvc Jul 22, 2024
6867dc6
Use MessageReferenceType in MessageReference initialiser
redmvc Jul 22, 2024
e58f341
Rename MessageSnapshot's timestamp to created_at
redmvc Jul 22, 2024
756f6c6
Create edited_at property in MessageSnapshot
redmvc Jul 22, 2024
6459700
Add missing line break
redmvc Jul 22, 2024
15b54ff
Pass 0 to try_enum in MessageReference.with_state()
redmvc Jul 22, 2024
73c6904
Use type directly in MessageReference init
redmvc Jul 22, 2024
bf1762c
Format message.py
redmvc Jul 23, 2024
8da22c0
Move MessageReferenceType definition
redmvc Jul 23, 2024
3e9abf4
Replace "snapshotted" with "forwarded" in docstrings
redmvc Jul 24, 2024
85969e1
Changes
DA-344 Sep 29, 2024
05da044
Merge branch 'master' of https://github.com/Rapptz/discord.py into fe…
DA-344 Sep 29, 2024
00de465
Moved MessageSnapshot to Discord Models
DA-344 Sep 29, 2024
f25b177
Removed trailing whitespace
DA-344 Sep 29, 2024
893730e
Added MessageReferenceType enum
DA-344 Sep 29, 2024
0f29fb1
Removed invalid escape sequence and formatted with black
DA-344 Sep 29, 2024
41e0cca
Added components and stickers to message snapshot
DA-344 Sep 29, 2024
7ed8568
Added stickers & components to attributes docs
DA-344 Sep 29, 2024
46ab672
Changes
DA-344 Sep 29, 2024
2f9940f
Run black
DA-344 Sep 29, 2024
17ab276
Update PartialMessage.forward
DA-344 Sep 29, 2024
081d4f7
Added missing versionadded
DA-344 Sep 30, 2024
4fdc1ae
Update Message.message_snapshots
DA-344 Oct 10, 2024
06a1abf
Update discord/enums.py
DA-344 Oct 10, 2024
74a675a
Merge branch 'master' of https://github.com/DA-344/d.py into feat/for…
DA-344 Oct 10, 2024
30e102b
Resolve merge conflicts
DA-344 Oct 10, 2024
340f74a
Run black
DA-344 Oct 10, 2024
a0a9687
Fix merge conflicts
DA-344 Oct 11, 2024
12c494d
Remove README changes
DA-344 Oct 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,13 @@ def __str__(self) -> str:
return self.name


class MessageReferenceType(Enum):
reply = 0
forward = 1

default = 0
DA-344 marked this conversation as resolved.
Show resolved Hide resolved


class MessageType(Enum):
default = 0
recipient_add = 1
Expand Down
225 changes: 218 additions & 7 deletions discord/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from typing import (
Dict,
TYPE_CHECKING,
Literal,
Sequence,
Union,
List,
Expand All @@ -49,7 +50,7 @@
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
Expand All @@ -72,6 +73,7 @@
Message as MessagePayload,
Attachment as AttachmentPayload,
MessageReference as MessageReferencePayload,
MessageSnapshot as MessageSnapshotPayload,
MessageApplication as MessageApplicationPayload,
MessageActivity as MessageActivityPayload,
RoleSubscriptionData as RoleSubscriptionDataPayload,
Expand Down Expand Up @@ -108,6 +110,7 @@
'PartialMessage',
'MessageInteraction',
'MessageReference',
'MessageSnapshot',
'DeletedReferencedMessage',
'MessageApplication',
'RoleSubscriptionInfo',
Expand Down Expand Up @@ -458,6 +461,133 @@ def guild_id(self) -> Optional[int]:
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]]],
):
if not message_snapshots:
return None

return [MessageSnapshot(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`.

Expand All @@ -468,6 +598,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`
Expand All @@ -492,10 +626,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
Expand All @@ -505,6 +648,7 @@ def __init__(self, *, message_id: int, channel_id: int, guild_id: Optional[int]
@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')
Expand All @@ -514,7 +658,13 @@ def with_state(cls, state: ConnectionState, data: MessageReferencePayload) -> Se
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
Expand All @@ -528,6 +678,10 @@ def from_message(cls, message: PartialMessage, *, fail_if_not_exists: bool = Tru
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
-------
Expand All @@ -539,6 +693,7 @@ def from_message(cls, message: PartialMessage, *, fail_if_not_exists: bool = Tru
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
Expand All @@ -561,7 +716,9 @@ def __repr__(self) -> str:
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
Expand Down Expand Up @@ -1593,7 +1750,12 @@ async def end_poll(self) -> Message:

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
Expand All @@ -1605,14 +1767,55 @@ def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference:
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
---------
:class:`~discord.MessageReference`
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 = {
Expand Down Expand Up @@ -1768,6 +1971,10 @@ class Message(PartialMessage, Hashable):
The poll attached to this message.

.. versionadded:: 2.4
message_snapshots: Optional[List[:class:`MessageSnapshot`]]
DA-344 marked this conversation as resolved.
Show resolved Hide resolved
The message snapshots attached to this message.

.. versionadded:: 2.5
"""

__slots__ = (
Expand Down Expand Up @@ -1804,6 +2011,7 @@ class Message(PartialMessage, Hashable):
'position',
'interaction_metadata',
'poll',
'message_snapshots',
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -1842,6 +2050,9 @@ def __init__(
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: Optional[List[MessageSnapshot]] = MessageSnapshot._from_value(
state, data.get('message_snapshots')
)

self.poll: Optional[Poll] = None
try:
Expand Down
18 changes: 18 additions & 0 deletions discord/types/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -154,6 +158,20 @@ class RoleSubscriptionData(TypedDict):
]


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
Expand Down
Loading
Loading