diff --git a/discord/abc.py b/discord/abc.py index ce811492bc..5d6c2ecc90 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1489,8 +1489,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``. @@ -1577,9 +1577,20 @@ async def send( allowed_mentions = allowed_mentions or AllowedMentions().to_dict() allowed_mentions["replied_user"] = bool(mention_author) + _reference = None + if reference is not None: try: - reference = reference.to_message_reference_dict() + _reference = reference.to_message_reference_dict() + from .message import MessageReference + + 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" @@ -1613,7 +1624,7 @@ async def send( embed=embed, embeds=embeds, nonce=nonce, - message_reference=reference, + message_reference=_reference, stickers=stickers, components=components, flags=flags, @@ -1639,7 +1650,7 @@ async def send( embeds=embeds, nonce=nonce, allowed_mentions=allowed_mentions, - message_reference=reference, + message_reference=_reference, stickers=stickers, components=components, flags=flags, @@ -1656,7 +1667,7 @@ async def send( embeds=embeds, nonce=nonce, allowed_mentions=allowed_mentions, - message_reference=reference, + message_reference=_reference, stickers=stickers, components=components, flags=flags, diff --git a/discord/enums.py b/discord/enums.py index f2bb6d3452..e96c809737 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -68,6 +68,7 @@ "AutoModKeywordPresetType", "ApplicationRoleConnectionMetadataType", "ReactionType", + "MessageReferenceType", ) @@ -257,6 +258,7 @@ class MessageType(Enum): stage_raise_hand = 30 stage_topic = 31 guild_application_premium_subscription = 32 + poll_result = 46 class VoiceRegion(Enum): @@ -952,6 +954,16 @@ class ReactionType(Enum): burst = 1 + +class MessageReferenceType(Enum): + """The message reference's type""" + + default = 0 + forward = 1 + + + + T = TypeVar("T") diff --git a/discord/message.py b/discord/message.py index c74f40c288..2e82ccd506 100644 --- a/discord/message.py +++ b/discord/message.py @@ -44,13 +44,14 @@ from .components import _component_factory from .embeds import Embed from .emoji import Emoji -from .enums import ChannelType, MessageType, try_enum +from .enums import ChannelType, MessageReferenceType, MessageType, try_enum from .errors import InvalidArgument from .file import File from .flags import MessageFlags from .guild import Guild from .member import Member from .mixins import Hashable +from .object import Object from .partial_emoji import PartialEmoji from .reaction import Reaction from .sticker import StickerItem @@ -74,10 +75,12 @@ from .types.member import Member as MemberPayload from .types.member import UserWithMember as UserWithMemberPayload from .types.message import Attachment as AttachmentPayload + from .types.message import ForwardedMessage as ForwardedMessagePayload from .types.message import Message as MessagePayload from .types.message import MessageActivity as MessageActivityPayload from .types.message import MessageApplication as MessageApplicationPayload from .types.message import MessageReference as MessageReferencePayload + from .types.message import MessageSnapshot as MessageSnapshotPayload from .types.message import Reaction as ReactionPayload from .types.threads import ThreadArchiveDuration from .types.user import User as UserPayload @@ -93,9 +96,9 @@ "PartialMessage", "MessageReference", "DeletedReferencedMessage", + "ForwardedMessage", ) - def convert_emoji_reaction(emoji): if isinstance(emoji, Reaction): emoji = emoji.emoji @@ -422,6 +425,10 @@ class MessageReference: Attributes ---------- + type: Optional[:class:`~discord.MessageReferenceType`] + The type of message reference. If this is not provided, assume default behavior (reply). + .. versionadded:: 2.7 + message_id: Optional[:class:`int`] The id of the message referenced. channel_id: :class:`int` @@ -452,6 +459,7 @@ class MessageReference: "guild_id", "fail_if_not_exists", "resolved", + "type", "_state", ) @@ -462,6 +470,7 @@ def __init__( channel_id: int, guild_id: int | None = None, fail_if_not_exists: bool = True, + type: MessageReferenceType = MessageReferenceType.default, ): self._state: ConnectionState | None = None self.resolved: Message | DeletedReferencedMessage | None = None @@ -469,14 +478,19 @@ def __init__( self.channel_id: int = channel_id self.guild_id: int | None = guild_id self.fail_if_not_exists: bool = fail_if_not_exists + self.type: MessageReferenceType = type @classmethod def with_state( cls: type[MR], state: ConnectionState, data: MessageReferencePayload ) -> MR: self = cls.__new__(cls) + 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 @@ -485,7 +499,11 @@ def with_state( @classmethod def from_message( - cls: type[MR], message: Message, *, fail_if_not_exists: bool = True + 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`. @@ -500,6 +518,9 @@ def from_message( if the message no longer exists or Discord could not fetch the message. .. versionadded:: 1.7 + type: Optional[:class:`~discord.MessageReferenceType`] + The type of reference to create. Defaults to :attr:`MessageReferenceType.default` (reply). + .. versionadded:: 2.7 Returns ------- @@ -511,6 +532,7 @@ def from_message( 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 @@ -532,7 +554,8 @@ def jump_url(self) -> str: def __repr__(self) -> str: return ( f"" + f" channel_id={self.channel_id!r} guild_id={self.guild_id!r}" + f" type={self.type!r}>" ) def to_dict(self) -> MessageReferencePayload: @@ -540,6 +563,7 @@ def to_dict(self) -> 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: @@ -548,6 +572,102 @@ def to_dict(self) -> MessageReferencePayload: to_message_reference_dict = to_dict +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. + """ + + def __init__( + self, + *, + state: ConnectionState, + reference: MessageReference, + data: ForwardedMessagePayload, + ): + self._state: ConnectionState = state + self._reference = reference + 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 + + def __repr__(self) -> str: + return f"" + + +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 | None + if fm := data.get("message"): + self.message = ForwardedMessage(state=state, reference=reference, data=fm) + + def flatten_handlers(cls): prefix = len("_handle_") @@ -685,6 +805,9 @@ class Message(Hashable): The thread created from this message, if applicable. .. versionadded:: 2.0 + snapshots: Optional[List[:class:`MessageSnapshots`]] + The snapshots attached to this message, if applicable. + .. versionadded:: 2.7 """ __slots__ = ( @@ -720,6 +843,7 @@ class Message(Hashable): "guild", "interaction", "thread", + "snapshots", ) if TYPE_CHECKING: @@ -799,6 +923,19 @@ 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 MessageInteraction self.interaction: MessageInteraction | None @@ -1766,9 +1903,41 @@ 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) - def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: + 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`] + 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) + ) + + 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 @@ -1781,6 +1950,11 @@ 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` @@ -1788,13 +1962,16 @@ def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: """ 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: diff --git a/discord/types/embed.py b/discord/types/embed.py index 0d73afe40c..b7aff9d25d 100644 --- a/discord/types/embed.py +++ b/discord/types/embed.py @@ -75,7 +75,14 @@ class EmbedAuthor(TypedDict, total=False): EmbedType = Literal[ - "rich", "image", "video", "gifv", "article", "link", "auto_moderation_message" + "rich", + "image", + "video", + "gifv", + "article", + "link", + "auto_moderation_message", + "poll_result", ] diff --git a/discord/types/message.py b/discord/types/message.py index 93e99ba7ab..f78df67ede 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -91,8 +91,10 @@ class MessageApplication(TypedDict): icon: str | None name: str +MessageReferenceType = Literal[0, 1] class MessageReference(TypedDict, total=False): + type: NotRequired[MessageReferenceType] message_id: Snowflake channel_id: Snowflake guild_id: Snowflake @@ -102,8 +104,28 @@ class MessageReference(TypedDict, total=False): MessageType = Literal[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 19, 20, 21, 22, 23, 24 ] +class MessageCall(TypedDict): + participants: SnowflakeList + ended_timestamp: NotRequired[str] +class ForwardedMessage(TypedDict): + type: MessageType + content: str + embeds: list[Embed] + attachments: list[Attachment] + timestamp: str + edited_timestamp: str | None + flags: NotRequired[int] + mentions: list[UserWithMember] + mention_roles: SnowflakeList + sticker_items: NotRequired[list[StickerItem]] + components: NotRequired[list[Component]] + + +class MessageSnapshot(TypedDict): + message: ForwardedMessage + class Message(TypedDict): guild_id: NotRequired[Snowflake] member: NotRequired[Member] @@ -135,6 +157,8 @@ class Message(TypedDict): embeds: list[Embed] pinned: bool type: MessageType + call: MessageCall + message_snapshots: NotRequired[list[MessageSnapshot]] AllowedMentionType = Literal["roles", "users", "everyone"]