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

feat: message forwarding #2598

Open
wants to merge 34 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7fb97fb
new message types
NeloBlivion Oct 6, 2024
58e7c6c
new embed type
NeloBlivion Oct 6, 2024
901bdfe
implement types
NeloBlivion Oct 6, 2024
ceaffaf
update MessageReference
NeloBlivion Oct 6, 2024
5baa513
forwarding
NeloBlivion Oct 6, 2024
8c0a3ee
Merge branch 'master' into forwarding
NeloBlivion Oct 6, 2024
b64ec06
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 6, 2024
c99efe6
s
NeloBlivion Oct 6, 2024
dde7436
fix
NeloBlivion Oct 6, 2024
ea21897
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 6, 2024
3f6be75
final?
NeloBlivion Oct 6, 2024
cd632c3
fix import
NeloBlivion Oct 6, 2024
7454697
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 6, 2024
4bb20d8
frfr
NeloBlivion Oct 6, 2024
7bbfd96
Merge branch 'master' into forwarding
NeloBlivion Oct 14, 2024
2e1c15a
Merge branch 'master' into forwarding
NeloBlivion Nov 6, 2024
e1cfad7
Merge branch 'master' into forwarding
NeloBlivion Nov 17, 2024
c7da3b8
conflict 1
NeloBlivion Feb 4, 2025
2d33242
Merge branch 'master' into forwarding
NeloBlivion Feb 4, 2025
a4657da
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 4, 2025
1d6b0bf
fix docstr
NeloBlivion Feb 4, 2025
acd29ef
adjustments
NeloBlivion Feb 5, 2025
ac39fc3
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 5, 2025
63f1675
_get_message
NeloBlivion Feb 5, 2025
389ffa7
fix reference kwarg
NeloBlivion Feb 5, 2025
0d49197
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 5, 2025
579751a
undo
NeloBlivion Feb 5, 2025
95474f8
add system_content support
NeloBlivion Feb 5, 2025
39652ab
adjustments
NeloBlivion Feb 5, 2025
8879fd6
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 5, 2025
01ef32e
changelog
NeloBlivion Feb 6, 2025
21c0e59
Merge branch 'master' into forwarding
NeloBlivion Feb 6, 2025
cf98e47
add has_snapshot flag
NeloBlivion Feb 6, 2025
9a115b1
Merge branch 'master' into forwarding
NeloBlivion Feb 7, 2025
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
Prev Previous commit
Next Next commit
forwarding
  • Loading branch information
NeloBlivion authored Oct 6, 2024
commit 5baa51389d3455cafb8a158ef7b850cabcbe1f3a
6 changes: 4 additions & 2 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1493,8 +1493,8 @@ async def send(
.. versionadded:: 1.4

reference: Union[:class:`~discord.Message`, :class:`~discord.MessageReference`, :class:`~discord.PartialMessage`]
A reference to the :class:`~discord.Message` to which you are replying, this can be created using
:meth:`~discord.Message.to_reference` or passed directly as a :class:`~discord.Message`. You can control
A reference to the :class:`~discord.Message` you are replying to or forwarding, this can be created using
:meth:`~discord.Message.to_reference` or passed directly as a :class:`~discord.Message`. When replying, you can control
whether this mentions the author of the referenced message using the
:attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions`` or by
setting ``mention_author``.
Expand Down Expand Up @@ -1588,6 +1588,8 @@ async def send(
if reference is not None:
try:
reference = reference.to_message_reference_dict()
if not isinstance(reference, MessageReference):
utils.warn_deprecated(f"Passing {type(reference).__name__} to reference", "MessageReference", "2.7", "3.0")
except AttributeError:
raise InvalidArgument(
"reference parameter must be Message, MessageReference, or"
Expand Down
2 changes: 2 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"EntitlementOwnerType",
"IntegrationType",
"InteractionContextType",
"PollLayoutType",
"MessageReferenceType",
)


Expand Down
183 changes: 169 additions & 14 deletions discord/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
from .types.message import MessageApplication as MessageApplicationPayload
from .types.message import MessageCall as MessageCallPayload
from .types.message import MessageReference as MessageReferencePayload
from .types.message import ForwardedMessage as ForwardedMessagePayload
from .types.message import MessageSnapshot as MessageSnapshotPayload
from .types.message import Reaction as ReactionPayload
from .types.poll import Poll as PollPayload
from .types.snowflake import SnowflakeList
Expand All @@ -101,6 +103,7 @@
"MessageReference",
"MessageCall",
"DeletedReferencedMessage",
"ForwardedMessage"
)


Expand Down Expand Up @@ -477,8 +480,8 @@ class MessageReference:

Attributes
----------
type: Optional[:class:`MessageReferenceType`]
The type of message reference. If this is not provided, assume default behavior.
type: Optional[:class:`~discord.MessageReferenceType`]
The type of message reference. If this is not provided, assume default behavior (reply).

.. versionadded:: 2.7

Expand Down Expand Up @@ -523,11 +526,11 @@ def __init__(
channel_id: int,
guild_id: int | None = None,
fail_if_not_exists: bool = True,
type: MessageReferenceType | None = None
type: MessageReferenceType = MessageReferenceType.default
):
self._state: ConnectionState | None = None
self.resolved: Message | DeletedReferencedMessage | None = None
self.type: MessageReferenceType | None = type
self.type: MessageReferenceType = type
self.message_id: int | None = message_id
self.channel_id: int = channel_id
self.guild_id: int | None = guild_id
Expand All @@ -538,9 +541,9 @@ def with_state(
cls: type[MR], state: ConnectionState, data: MessageReferencePayload
) -> MR:
self = cls.__new__(cls)
self.type = try_enum(MessageReferenceType, data.get("type"))
self.type = try_enum(MessageReferenceType, data.get("type")) or MessageReferenceType.default
self.message_id = utils._get_as_snowflake(data, "message_id")
self.channel_id = int(data.pop("channel_id"))
self.channel_id = utils._get_as_snowflake(data, "channel_id")
self.guild_id = utils._get_as_snowflake(data, "guild_id")
self.fail_if_not_exists = data.get("fail_if_not_exists", True)
self._state = state
Expand All @@ -549,7 +552,7 @@ def with_state(

@classmethod
def from_message(
cls: type[MR], message: Message, *, fail_if_not_exists: bool = True, type: MessageReferenceType = None
cls: type[MR], message: Message, *, fail_if_not_exists: bool = True, type: MessageReferenceType = MessageReferenceType.default
) -> MR:
"""Creates a :class:`MessageReference` from an existing :class:`~discord.Message`.

Expand All @@ -565,8 +568,8 @@ def from_message(

.. versionadded:: 1.7

type: Optional[:class:`MessageReferenceType`]
The type of reference to create. Defaults to reply.
type: Optional[:class:`~discord.MessageReferenceType`]
The type of reference to create. Defaults to :attr:`MessageReferenceType.default` (reply).

.. versionadded:: 2.7

Expand Down Expand Up @@ -602,14 +605,16 @@ def jump_url(self) -> str:
def __repr__(self) -> str:
return (
f"<MessageReference message_id={self.message_id!r}"
f" channel_id={self.channel_id!r} guild_id={self.guild_id!r}>"
f" channel_id={self.channel_id!r} guild_id={self.guild_id!r}"
f" type={self.type!r}>"
)

def to_dict(self) -> MessageReferencePayload:
result: MessageReferencePayload = (
{"message_id": self.message_id} if self.message_id is not None else {}
)
result["channel_id"] = self.channel_id
result["type"] = self.type and self.type.value
if self.guild_id is not None:
result["guild_id"] = self.guild_id
if self.fail_if_not_exists is not None:
Expand Down Expand Up @@ -647,6 +652,106 @@ def ended_at(self) -> datetime.datetime | None:
return self._ended_timestamp


class ForwardedMessage:
"""Represents the snapshotted contents from a forwarded message. Forwarded messages are immutable; any updates to the original message won't be reflected.

.. versionadded:: 2.7

Attributes
----------
type: :class:`MessageType`
The type of message. In most cases this should not be checked, but it is helpful
in cases where it might be a system message for :attr:`system_content`.
content: :class:`str`
The contents of the original message.
embeds: List[:class:`Embed`]
A list of embeds the original message had.
attachments: List[:class:`Attachment`]
A list of attachments given to the original message.
flags: :class:`MessageFlags`
Extra features of the message.
mentions: List[Union[:class:`abc.User`, :class:`Object`]]
A list of :class:`Member` that were mentioned.
role_mentions: List[Union[:class:`Role`, :class:`Object`]]
A list of :class:`Role` that were mentioned.
stickers: List[:class:`StickerItem`]
A list of sticker items given to the original message.
components: List[:class:`Component`]
A list of components in the original message.
"""

__slots__ = (
"message_id",
"channel_id",
"guild_id",
"fail_if_not_exists",
"resolved",
"type",
"_state",
)

def __init__(
self,
*,
state: ConnectionState,
reference: MessageReference,
data: ForwardedMessagePayload,
):
self._state: ConnectionState = state
self.id: int = reference.message_id
self.channel = state.get_channel(reference.channel_id) or (reference.channel_id and Object(reference.channel_id))
self.guild = state._get_guild(reference.guild_id) or (reference.guild_id and Object(reference.guild_id))
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.flags: MessageFlags = MessageFlags._from_value(data.get("flags", 0))
self.stickers: list[StickerItem] = [
StickerItem(data=d, state=state) for d in data.get("sticker_items", [])
]
self.components: list[Component] = [
_component_factory(d) for d in data.get("components", [])
]
self._edited_timestamp: datetime.datetime | None = utils.parse_time(
data["edited_timestamp"]
)

@property
def created_at(self) -> datetime.datetime:
"""The original message's creation time in UTC."""
return utils.snowflake_time(self.id)

@property
def edited_at(self) -> datetime.datetime | None:
"""An aware UTC datetime object containing the
edited time of the original message.
"""
return self._edited_timestamp


class MessageSnapshot:
"""Represents a message snapshot.

.. versionadded:: 2.7

Attributes
----------
message: :class:`ForwardedMessage`
The forwarded message, which includes a minimal subset of fields from the original message.
"""

def __init__(
self,
*,
state: ConnectionState,
reference: MessageReference,
data: MessageSnapshotPayload,
):
self._state: ConnectionState = state
self.message: ForwardedMessage = ForwardedMessage(state=state, reference=reference, data=data)


def flatten_handlers(cls):
prefix = len("_handle_")
handlers = [
Expand Down Expand Up @@ -799,6 +904,10 @@ class Message(Hashable):
The call information associated with this message, if applicable.

.. versionadded:: 2.6
snapshots: Optional[List[:class:`MessageSnapshots`]]
The snapshots attached to this message, if applicable.

.. versionadded:: 2.7
"""

__slots__ = (
Expand Down Expand Up @@ -837,6 +946,7 @@ class Message(Hashable):
"thread",
"_poll",
"call",
"snapshots",
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -916,6 +1026,14 @@ def __init__(
# the channel will be the correct type here
ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore

self.snapshots: list[MessageSnapshot]
try:
self.snapshots = [MessageSnapshot(
state=state, reference=self.reference, data=ms,
) for ms in data["message_snapshots"]]
except KeyError:
self.snapshots = []

from .interactions import InteractionMetadata, MessageInteraction

self._interaction: MessageInteraction | None
Expand Down Expand Up @@ -1939,7 +2057,38 @@ async def reply(self, content: str | None = None, **kwargs) -> Message:
you specified both ``file`` and ``files``.
"""

return await self.channel.send(content, reference=self, **kwargs)
return await self.channel.send(content, reference=self.to_reference(), **kwargs)

async def forward(self, channel: MessageableChannel | PartialMessageableChannel, **kwargs) -> Message:
"""|coro|

A shortcut method to :meth:`.abc.Messageable.send` to forward the
:class:`.Message` to a channel.

.. versionadded:: 2.7

Parameters
----------
channel: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, :class:`str`]
NeloBlivion marked this conversation as resolved.
Show resolved Hide resolved
The emoji to react with.

Returns
-------
:class:`.Message`
The message that was sent.

Raises
------
~discord.HTTPException
Sending the message failed.
~discord.Forbidden
You do not have the proper permissions to send the message.
~discord.InvalidArgument
The ``files`` list is not of the appropriate size, or
you specified both ``file`` and ``files``.
"""

return await channel.send(reference=self.to_reference(type=MessageReferenceType.forward))

async def end_poll(self) -> Message:
"""|coro|
Expand Down Expand Up @@ -1969,7 +2118,7 @@ async def end_poll(self) -> Message:

return message

def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference:
def to_reference(self, *, fail_if_not_exists: bool = True, type: MessageReferenceType = None) -> MessageReference:
"""Creates a :class:`~discord.MessageReference` from the current message.

.. versionadded:: 1.6
Expand All @@ -1982,20 +2131,26 @@ def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference:

.. versionadded:: 1.7

type: Optional[:class:`~discord.MessageReferenceType`]
The type of message reference. Defaults to a reply.

.. versionadded:: 2.7

Returns
-------
:class:`~discord.MessageReference`
The reference to this message.
"""

return MessageReference.from_message(
self, fail_if_not_exists=fail_if_not_exists
self, fail_if_not_exists=fail_if_not_exists, type=type
)

def to_message_reference_dict(self) -> MessageReferencePayload:
def to_message_reference_dict(self, type: MessageReferenceType = None) -> MessageReferencePayload:
data: MessageReferencePayload = {
"message_id": self.id,
"channel_id": self.channel.id,
"type": type and type.value,
}

if self.guild is not None:
Expand Down
Loading