diff --git a/discord/message.py b/discord/message.py index 12783ea9a..42732d8bf 100644 --- a/discord/message.py +++ b/discord/message.py @@ -83,7 +83,7 @@ if TYPE_CHECKING: ) from .types.user import User as UserPayload from .types.embed import Embed as EmbedPayload - from .types.gateway import MessageReactionRemoveEvent + from .types.gateway import MessageReactionRemoveEvent, MessageUpdateEvent from .abc import Snowflake from .abc import GuildChannel, PartialMessageableChannel, MessageableChannel from .components import Component @@ -454,7 +454,7 @@ class MessageReference: return self @classmethod - def from_message(cls, message: Message, *, fail_if_not_exists: bool = True) -> Self: + def from_message(cls, message: PartialMessage, *, fail_if_not_exists: bool = True) -> Self: """Creates a :class:`MessageReference` from an existing :class:`~discord.Message`. .. versionadded:: 1.6 @@ -527,635 +527,109 @@ def flatten_handlers(cls: Type[Message]) -> Type[Message]: return cls -@flatten_handlers -class Message(Hashable): - r"""Represents a message from Discord. +class PartialMessage(Hashable): + """Represents a partial message to aid with working messages when only + a message and channel ID are present. + + There are two ways to construct this class. The first one is through + the constructor itself, and the second is via the following: + + - :meth:`TextChannel.get_partial_message` + - :meth:`Thread.get_partial_message` + - :meth:`DMChannel.get_partial_message` + + Note that this class is trimmed down and has no rich attributes. + + .. versionadded:: 1.6 .. container:: operations .. describe:: x == y - Checks if two messages are equal. + Checks if two partial messages are equal. .. describe:: x != y - Checks if two messages are not equal. + Checks if two partial messages are not equal. .. describe:: hash(x) - Returns the message's hash. + Returns the partial message's hash. Attributes ----------- - tts: :class:`bool` - Specifies if the message was done with text-to-speech. - This can only be accurately received in :func:`on_message` due to - a discord limitation. - 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`. - author: Union[:class:`Member`, :class:`abc.User`] - A :class:`Member` that sent the message. If :attr:`channel` is a - private channel or the user has the left the guild, then it is a :class:`User` instead. - content: :class:`str` - The actual contents of the message. - nonce: Optional[Union[:class:`str`, :class:`int`]] - The value used by the discord guild and the client to verify that the message is successfully sent. - This is not stored long term within Discord's servers and is only used ephemerally. - embeds: List[:class:`Embed`] - A list of embeds the message has. - channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`, :class:`PartialMessageable`] - The :class:`TextChannel` or :class:`Thread` that the message was sent from. - Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. - reference: Optional[:class:`~discord.MessageReference`] - The message that this message references. This is only applicable to messages of - type :attr:`MessageType.pins_add`, crossposted messages created by a - followed channel integration, or message replies. - - .. versionadded:: 1.5 - - mention_everyone: :class:`bool` - Specifies if the message mentions everyone. - - .. note:: - - This does not check if the ``@everyone`` or the ``@here`` text is in the message itself. - Rather this boolean indicates if either the ``@everyone`` or the ``@here`` text is in the message - **and** it did end up mentioning. - mentions: List[:class:`abc.User`] - A list of :class:`Member` that were mentioned. If the message is in a private message - then the list will be of :class:`User` instead. For messages that are not of type - :attr:`MessageType.default`\, this array can be used to aid in system messages. - For more information, see :attr:`system_content`. - - .. warning:: - - The order of the mentions list is not in any particular order so you should - not rely on it. This is a Discord limitation, not one with the library. - channel_mentions: List[Union[:class:`abc.GuildChannel`, :class:`Thread`]] - A list of :class:`abc.GuildChannel` or :class:`Thread` that were mentioned. If the message is - in a private message then the list is always empty. - role_mentions: List[:class:`Role`] - A list of :class:`Role` that were mentioned. If the message is in a private message - then the list is always empty. + channel: Union[:class:`PartialMessageable`, :class:`TextChannel`, :class:`Thread`, :class:`DMChannel`] + The channel associated with this partial message. id: :class:`int` The message ID. - webhook_id: Optional[:class:`int`] - If this message was sent by a webhook, then this is the webhook ID's that sent this - message. - attachments: List[:class:`Attachment`] - A list of attachments given to a message. - pinned: :class:`bool` - Specifies if the message is currently pinned. - flags: :class:`MessageFlags` - Extra features of the message. - - .. versionadded:: 1.3 - - reactions : List[:class:`Reaction`] - Reactions to a message. Reactions can be either custom emoji or standard unicode emoji. - activity: Optional[:class:`dict`] - The activity associated with this message. Sent with Rich-Presence related messages that for - example, request joining, spectating, or listening to or with another member. - - It is a dictionary with the following optional keys: - - - ``type``: An integer denoting the type of message activity being requested. - - ``party_id``: The party ID associated with the party. - application: Optional[:class:`dict`] - The rich presence enabled application associated with this message. - - It is a dictionary with the following keys: - - - ``id``: A string representing the application's ID. - - ``name``: A string representing the application's name. - - ``description``: A string representing the application's description. - - ``icon``: A string representing the icon ID of the application. - - ``cover_image``: A string representing the embed's image asset ID. - stickers: List[:class:`StickerItem`] - A list of sticker items given to the message. - - .. versionadded:: 1.6 - components: List[:class:`Component`] - A list of components in the message. - - .. versionadded:: 2.0 guild: Optional[:class:`Guild`] - The guild that the message belongs to, if applicable. + The guild that the partial message belongs to, if applicable. """ - __slots__ = ( - '_state', - '_edited_timestamp', - '_cs_channel_mentions', - '_cs_raw_mentions', - '_cs_clean_content', - '_cs_raw_channel_mentions', - '_cs_raw_role_mentions', - '_cs_system_content', - 'tts', - 'content', - 'channel', - 'webhook_id', - 'mention_everyone', - 'embeds', - 'id', - 'mentions', - 'author', - 'attachments', - 'nonce', - 'pinned', - 'role_mentions', - 'type', - 'flags', - 'reactions', - 'reference', - 'application', - 'activity', - 'stickers', - 'components', - 'guild', - ) + __slots__ = ('channel', 'id', '_cs_guild', '_state', 'guild') - if TYPE_CHECKING: - _HANDLERS: ClassVar[List[Tuple[str, Callable[..., None]]]] - _CACHED_SLOTS: ClassVar[List[str]] - guild: Optional[Guild] - reference: Optional[MessageReference] - mentions: List[Union[User, Member]] - author: Union[User, Member] - role_mentions: List[Role] + def __init__(self, *, channel: MessageableChannel, id: int) -> None: + if not isinstance(channel, PartialMessageable) and channel.type not in ( + ChannelType.text, + ChannelType.news, + ChannelType.private, + ChannelType.news_thread, + ChannelType.public_thread, + ChannelType.private_thread, + ): + raise TypeError(f'Expected PartialMessageable, TextChannel, DMChannel or Thread not {type(channel)!r}') - def __init__( - self, - *, - state: ConnectionState, - channel: MessageableChannel, - data: MessagePayload, - ): - self._state: ConnectionState = state - self.id: int = int(data['id']) - self.webhook_id: Optional[int] = utils._get_as_snowflake(data, 'webhook_id') - self.reactions: List[Reaction] = [Reaction(message=self, data=d) for d in data.get('reactions', [])] - self.attachments: List[Attachment] = [Attachment(data=a, state=self._state) for a in data['attachments']] - self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']] - self.application: Optional[MessageApplicationPayload] = data.get('application') - self.activity: Optional[MessageActivityPayload] = data.get('activity') self.channel: MessageableChannel = channel - self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp']) - self.type: MessageType = try_enum(MessageType, data['type']) - self.pinned: bool = data['pinned'] - self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0)) - self.mention_everyone: bool = data['mention_everyone'] - self.tts: bool = data['tts'] - self.content: str = data['content'] - self.nonce: Optional[Union[int, str]] = data.get('nonce') - 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._state: ConnectionState = channel._state + self.id: int = id - try: - # if the channel doesn't have a guild attribute, we handle that - self.guild = channel.guild # type: ignore - except AttributeError: - self.guild = state._get_guild(utils._get_as_snowflake(data, 'guild_id')) + self.guild: Optional[Guild] = getattr(channel, 'guild', None) - try: - ref = data['message_reference'] - except KeyError: - self.reference = None - else: - self.reference = ref = MessageReference.with_state(state, ref) - try: - resolved = data['referenced_message'] - except KeyError: - pass - else: - if resolved is None: - ref.resolved = DeletedReferencedMessage(ref) - else: - # Right now the channel IDs match but maybe in the future they won't. - if ref.channel_id == channel.id: - chan = channel - elif isinstance(channel, Thread) and channel.parent_id == ref.channel_id: - chan = channel - else: - chan, _ = state._get_guild_channel(resolved, ref.guild_id) + def _update(self, data: MessageUpdateEvent) -> None: + # This is used for duck typing purposes. + # Just do nothing with the data. + pass - # the channel will be the correct type here - ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore - - for handler in ('author', 'member', 'mentions', 'mention_roles'): - try: - getattr(self, f'_handle_{handler}')(data[handler]) - except KeyError: - continue + # Also needed for duck typing purposes + # n.b. not exposed + pinned: Any = property(None, lambda x, y: None) def __repr__(self) -> str: - name = self.__class__.__name__ - return ( - f'<{name} id={self.id} channel={self.channel!r} type={self.type!r} author={self.author!r} flags={self.flags!r}>' - ) - - def _try_patch(self, data, key, transform=None) -> None: - try: - value = data[key] - except KeyError: - pass - else: - if transform is None: - setattr(self, key, value) - else: - setattr(self, key, transform(value)) - - def _add_reaction(self, data, emoji, user_id) -> Reaction: - reaction = utils.find(lambda r: r.emoji == emoji, self.reactions) - is_me = data['me'] = user_id == self._state.self_id - - if reaction is None: - reaction = Reaction(message=self, data=data, emoji=emoji) - self.reactions.append(reaction) - else: - reaction.count += 1 - if is_me: - reaction.me = is_me - - return reaction - - def _remove_reaction(self, data: MessageReactionRemoveEvent, emoji: EmojiInputType, user_id: int) -> Reaction: - reaction = utils.find(lambda r: r.emoji == emoji, self.reactions) - - if reaction is None: - # already removed? - raise ValueError('Emoji already removed?') - - # if reaction isn't in the list, we crash. This means discord - # sent bad data, or we stored improperly - reaction.count -= 1 - - if user_id == self._state.self_id: - reaction.me = False - if reaction.count == 0: - # this raises ValueError if something went wrong as well. - self.reactions.remove(reaction) - - return reaction - - def _clear_emoji(self, emoji) -> Optional[Reaction]: - to_check = str(emoji) - for index, reaction in enumerate(self.reactions): - if str(reaction.emoji) == to_check: - break - else: - # didn't find anything so just return - return - - del self.reactions[index] - return reaction - - def _update(self, data): - # In an update scheme, 'author' key has to be handled before 'member' - # otherwise they overwrite each other which is undesirable. - # Since there's no good way to do this we have to iterate over every - # handler rather than iterating over the keys which is a little slower - for key, handler in self._HANDLERS: - try: - value = data[key] - except KeyError: - continue - else: - handler(self, value) - - # clear the cached properties - for attr in self._CACHED_SLOTS: - try: - delattr(self, attr) - except AttributeError: - pass - - def _handle_edited_timestamp(self, value: str) -> None: - self._edited_timestamp = utils.parse_time(value) - - def _handle_pinned(self, value: bool) -> None: - self.pinned = value - - def _handle_flags(self, value: int) -> None: - self.flags = MessageFlags._from_value(value) - - def _handle_application(self, value: MessageApplicationPayload) -> None: - self.application = value - - def _handle_activity(self, value: MessageActivityPayload) -> None: - self.activity = value - - def _handle_mention_everyone(self, value: bool) -> None: - self.mention_everyone = value - - def _handle_tts(self, value: bool) -> None: - self.tts = value - - def _handle_type(self, value: int) -> None: - self.type = try_enum(MessageType, value) - - def _handle_content(self, value: str) -> None: - self.content = value - - def _handle_attachments(self, value: List[AttachmentPayload]) -> None: - self.attachments = [Attachment(data=a, state=self._state) for a in value] - - def _handle_embeds(self, value: List[EmbedPayload]) -> None: - self.embeds = [Embed.from_dict(data) for data in value] - - def _handle_nonce(self, value: Union[str, int]) -> None: - self.nonce = value - - def _handle_author(self, author: UserPayload) -> None: - self.author = self._state.store_user(author) - if isinstance(self.guild, Guild): - found = self.guild.get_member(self.author.id) - if found is not None: - self.author = found - - def _handle_member(self, member: MemberPayload) -> None: - # The gateway now gives us full Member objects sometimes with the following keys - # deaf, mute, joined_at, roles - # For the sake of performance I'm going to assume that the only - # field that needs *updating* would be the joined_at field. - # If there is no Member object (for some strange reason), then we can upgrade - # ourselves to a more "partial" member object. - author = self.author - try: - # Update member reference - author._update_from_message(member) # type: ignore - except AttributeError: - # It's a user here - # TODO: consider adding to cache here - self.author = Member._from_message(message=self, data=member) - - def _handle_mentions(self, mentions: List[UserWithMemberPayload]) -> None: - self.mentions = r = [] - guild = self.guild - state = self._state - if not isinstance(guild, Guild): - self.mentions = [state.store_user(m) for m in mentions] - return - - for mention in filter(None, mentions): - id_search = int(mention['id']) - member = guild.get_member(id_search) - if member is not None: - r.append(member) - else: - r.append(Member._try_upgrade(data=mention, guild=guild, state=state)) - - def _handle_mention_roles(self, role_mentions: List[int]) -> None: - self.role_mentions = [] - if isinstance(self.guild, Guild): - for role_id in map(int, role_mentions): - role = self.guild.get_role(role_id) - if role is not None: - self.role_mentions.append(role) - - def _handle_components(self, components: List[ComponentPayload]): - self.components = [_component_factory(d) for d in components] - - def _rebind_cached_references(self, new_guild: Guild, new_channel: Union[TextChannel, Thread]) -> None: - self.guild = new_guild - self.channel = new_channel - - @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_channel_mentions') - def channel_mentions(self) -> List[Union[GuildChannel, Thread]]: - if self.guild is None: - return [] - it = filter(None, map(self.guild._resolve_channel, self.raw_channel_mentions)) - return utils._unique(it) - - @utils.cached_slot_property('_cs_clean_content') - def clean_content(self) -> str: - """:class:`str`: A property that returns the content in a "cleaned up" - manner. This basically means that mentions are transformed - into the way the client shows it. e.g. ``<#id>`` will transform - into ``#name``. - - This will also transform @everyone and @here mentions into - non-mentions. - - .. note:: - - This *does not* affect markdown. If you want to escape - or remove markdown then use :func:`utils.escape_markdown` or :func:`utils.remove_markdown` - respectively, along with this function. - """ - - if self.guild: - - def resolve_member(id: int) -> str: - m = self.guild.get_member(id) or utils.get(self.mentions, id=id) - return f'@{m.display_name}' if m else '@deleted-user' - - def resolve_role(id: int) -> str: - r = self.guild.get_role(id) or utils.get(self.role_mentions, id=id) - return f'@{r.name}' if r else '@deleted-role' - - def resolve_channel(id: int) -> str: - c = self.guild._resolve_channel(id) - return f'#{c.name}' if c else '#deleted-channel' - - else: - - def resolve_member(id: int) -> str: - m = utils.get(self.mentions, id=id) - return f'@{m.display_name}' if m else '@deleted-user' - - def resolve_role(id: int) -> str: - return '@deleted-role' - - def resolve_channel(id: int) -> str: - return f'#deleted-channel' - - transforms = { - '@': resolve_member, - '@!': resolve_member, - '#': resolve_channel, - '@&': resolve_role, - } - - def repl(match: re.Match) -> str: - type = match[1] - id = int(match[2]) - transformed = transforms[type](id) - return transformed - - result = re.sub(r'<(@[!&]?|#)([0-9]{15,20})>', repl, self.content) - - return escape_mentions(result) + return f'' @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: The message's creation time in UTC.""" + """:class:`datetime.datetime`: The partial message's creation time in UTC.""" return utils.snowflake_time(self.id) - @property - def edited_at(self) -> Optional[datetime.datetime]: - """Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the edited time of the message.""" - return self._edited_timestamp - @property def jump_url(self) -> str: """:class:`str`: Returns a URL that allows the client to jump to this message.""" guild_id = getattr(self.guild, 'id', '@me') return f'https://discord.com/channels/{guild_id}/{self.channel.id}/{self.id}' - def is_system(self) -> bool: - """:class:`bool`: Whether the message is a system message. + async def fetch(self) -> Message: + """|coro| - A system message is a message that is constructed entirely by the Discord API - in response to something. + Fetches the partial message to a full :class:`Message`. - .. versionadded:: 1.3 - """ - return self.type not in ( - MessageType.default, - MessageType.reply, - MessageType.application_command, - MessageType.thread_starter_message, - ) + Raises + -------- + NotFound + The message was not found. + Forbidden + You do not have the permissions required to get a message. + HTTPException + Retrieving the message failed. - @utils.cached_slot_property('_cs_system_content') - def system_content(self) -> Optional[str]: - r""":class:`str`: A property that returns the content that is rendered - regardless of the :attr:`Message.type`. - - In the case of :attr:`MessageType.default` and :attr:`MessageType.reply`\, - this just returns the regular :attr:`Message.content`. Otherwise this - returns an English message denoting the contents of the system message. + Returns + -------- + :class:`Message` + The full message. """ - if self.type is MessageType.default: - return self.content - - if self.type is MessageType.recipient_add: - if self.channel.type is ChannelType.group: - return f'{self.author.name} added {self.mentions[0].name} to the group.' - else: - return f'{self.author.name} added {self.mentions[0].name} to the thread.' - - if self.type is MessageType.recipient_remove: - if self.channel.type is ChannelType.group: - return f'{self.author.name} removed {self.mentions[0].name} from the group.' - else: - return f'{self.author.name} removed {self.mentions[0].name} from the thread.' - - if self.type is MessageType.channel_name_change: - return f'{self.author.name} changed the channel name: **{self.content}**' - - if self.type is MessageType.channel_icon_change: - return f'{self.author.name} changed the channel icon.' - - if self.type is MessageType.pins_add: - return f'{self.author.name} pinned a message to this channel.' - - if self.type is MessageType.new_member: - formats = [ - "{0} joined the party.", - "{0} is here.", - "Welcome, {0}. We hope you brought pizza.", - "A wild {0} appeared.", - "{0} just landed.", - "{0} just slid into the server.", - "{0} just showed up!", - "Welcome {0}. Say hi!", - "{0} hopped into the server.", - "Everyone welcome {0}!", - "Glad you're here, {0}.", - "Good to see you, {0}.", - "Yay you made it, {0}!", - ] - - created_at_ms = int(self.created_at.timestamp() * 1000) - return formats[created_at_ms % len(formats)].format(self.author.name) - - if self.type is MessageType.premium_guild_subscription: - if not self.content: - return f'{self.author.name} just boosted the server!' - else: - return f'{self.author.name} just boosted the server **{self.content}** times!' - - if self.type is MessageType.premium_guild_tier_1: - if not self.content: - return f'{self.author.name} just boosted the server! {self.guild} has achieved **Level 1!**' - else: - return f'{self.author.name} just boosted the server **{self.content}** times! {self.guild} has achieved **Level 1!**' - - if self.type is MessageType.premium_guild_tier_2: - if not self.content: - return f'{self.author.name} just boosted the server! {self.guild} has achieved **Level 2!**' - else: - return f'{self.author.name} just boosted the server **{self.content}** times! {self.guild} has achieved **Level 2!**' - - if self.type is MessageType.premium_guild_tier_3: - if not self.content: - return f'{self.author.name} just boosted the server! {self.guild} has achieved **Level 3!**' - else: - return f'{self.author.name} just boosted the server **{self.content}** times! {self.guild} has achieved **Level 3!**' - - if self.type is MessageType.channel_follow_add: - return f'{self.author.name} has added {self.content} to this channel' - - if self.type is MessageType.guild_stream: - # the author will be a Member - return f'{self.author.name} is live! Now streaming {self.author.activity.name}' # type: ignore - - if self.type is MessageType.guild_discovery_disqualified: - return 'This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details.' - - if self.type is MessageType.guild_discovery_requalified: - return 'This server is eligible for Server Discovery again and has been automatically relisted!' - - if self.type is MessageType.guild_discovery_grace_period_initial_warning: - return 'This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery.' - - if self.type is MessageType.guild_discovery_grace_period_final_warning: - return 'This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.' - - if self.type is MessageType.thread_created: - return f'{self.author.name} started a thread: **{self.content}**. See all **threads**.' - - if self.type is MessageType.reply: - return self.content - - if self.type is MessageType.thread_starter_message: - if self.reference is None or self.reference.resolved is None: - return 'Sorry, we couldn\'t load the first message in this thread' - - # the resolved message for the reference will be a Message - return self.reference.resolved.content # type: ignore - - if self.type is MessageType.guild_invite_reminder: - return 'Wondering who to invite?\nStart by inviting anyone who can help you build the server!' + data = await self._state.http.get_message(self.channel.id, self.id) + return self._state.create_message(channel=self.channel, data=data) async def delete(self, *, delay: Optional[float] = None) -> None: """|coro| @@ -1204,7 +678,6 @@ class Message(Hashable): content: Optional[str] = ..., embed: Optional[Embed] = ..., attachments: Sequence[Union[Attachment, File]] = ..., - suppress: bool = ..., delete_after: Optional[float] = ..., allowed_mentions: Optional[AllowedMentions] = ..., view: Optional[View] = ..., @@ -1218,7 +691,6 @@ class Message(Hashable): content: Optional[str] = ..., embeds: Sequence[Embed] = ..., attachments: Sequence[Union[Attachment, File]] = ..., - suppress: bool = ..., delete_after: Optional[float] = ..., allowed_mentions: Optional[AllowedMentions] = ..., view: Optional[View] = ..., @@ -1231,7 +703,6 @@ class Message(Hashable): embed: Optional[Embed] = MISSING, embeds: Sequence[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - suppress: bool = MISSING, delete_after: Optional[float] = None, allowed_mentions: Optional[AllowedMentions] = MISSING, view: Optional[View] = MISSING, @@ -1242,9 +713,6 @@ class Message(Hashable): The content must be able to be transformed into a string via ``str(content)``. - .. versionchanged:: 1.3 - The ``suppress`` keyword-only parameter was added. - .. versionchanged:: 2.0 Edits are no longer in-place, the newly edited message is returned instead. @@ -1274,11 +742,6 @@ class Message(Hashable): New files will always appear after current attachments. .. versionadded:: 2.0 - suppress: :class:`bool` - Whether to suppress embeds for the message. This removes - all the embeds if set to ``True``. If set to ``False`` - this brings the embeds back if they were suppressed. - Using this parameter requires :attr:`~.Permissions.manage_messages`. delete_after: Optional[:class:`float`] If provided, the number of seconds to wait in the background before deleting the message we just edited. If the deletion fails, @@ -1317,18 +780,11 @@ class Message(Hashable): else: previous_allowed_mentions = None - if suppress is not MISSING: - flags = MessageFlags._from_value(self.flags.value) - flags.suppress_embeds = suppress - else: - flags = MISSING - if view is not MISSING: self._state.prevent_view_updates_for(self.id) params = handle_message_parameters( content=content, - flags=flags, embed=embed, embeds=embeds, attachments=attachments, @@ -1347,58 +803,6 @@ class Message(Hashable): return message - async def add_files(self, *files: File) -> Message: - r"""|coro| - - Adds new files to the end of the message attachments. - - .. versionadded:: 2.0 - - Parameters - ----------- - \*files: :class:`File` - New files to add to the message. - - Raises - ------- - HTTPException - Editing the message failed. - Forbidden - Tried to edit a message that isn't yours. - - Returns - -------- - :class:`Message` - The newly edited message. - """ - return await self.edit(attachments=[*self.attachments, *files]) - - async def remove_attachments(self, *attachments: Attachment) -> Message: - r"""|coro| - - Removes attachments from the message. - - .. versionadded:: 2.0 - - Parameters - ----------- - \*attachments: :class:`Attachment` - Attachments to remove from the message. - - Raises - ------- - HTTPException - Editing the message failed. - Forbidden - Tried to edit a message that isn't yours. - - Returns - -------- - :class:`Message` - The newly edited message. - """ - return await self.edit(attachments=[a for a in self.attachments if a not in attachments]) - async def publish(self) -> None: """|coro| @@ -1446,6 +850,7 @@ class Message(Hashable): """ await self._state.http.pin_message(self.channel.id, self.id, reason=reason) + # pinned exists on PartialMessage for duck typing purposes self.pinned = True async def unpin(self, *, reason: Optional[str] = None) -> None: @@ -1474,6 +879,7 @@ class Message(Hashable): """ await self._state.http.unpin_message(self.channel.id, self.id, reason=reason) + # pinned exists on PartialMessage for duck typing purposes self.pinned = False async def add_reaction(self, emoji: EmojiInputType, /) -> None: @@ -1734,117 +1140,826 @@ class Message(Hashable): return data -class PartialMessage(Hashable): - """Represents a partial message to aid with working messages when only - a message and channel ID are present. - - There are two ways to construct this class. The first one is through - the constructor itself, and the second is via the following: - - - :meth:`TextChannel.get_partial_message` - - :meth:`Thread.get_partial_message` - - :meth:`DMChannel.get_partial_message` - - Note that this class is trimmed down and has no rich attributes. - - .. versionadded:: 1.6 +@flatten_handlers +class Message(PartialMessage, Hashable): + r"""Represents a message from Discord. .. container:: operations .. describe:: x == y - Checks if two partial messages are equal. + Checks if two messages are equal. .. describe:: x != y - Checks if two partial messages are not equal. + Checks if two messages are not equal. .. describe:: hash(x) - Returns the partial message's hash. + Returns the message's hash. Attributes ----------- - channel: Union[:class:`PartialMessageable`, :class:`TextChannel`, :class:`Thread`, :class:`DMChannel`] - The channel associated with this partial message. + tts: :class:`bool` + Specifies if the message was done with text-to-speech. + This can only be accurately received in :func:`on_message` due to + a discord limitation. + 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`. + author: Union[:class:`Member`, :class:`abc.User`] + A :class:`Member` that sent the message. If :attr:`channel` is a + private channel or the user has the left the guild, then it is a :class:`User` instead. + content: :class:`str` + The actual contents of the message. + nonce: Optional[Union[:class:`str`, :class:`int`]] + The value used by the discord guild and the client to verify that the message is successfully sent. + This is not stored long term within Discord's servers and is only used ephemerally. + embeds: List[:class:`Embed`] + A list of embeds the message has. + channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`, :class:`PartialMessageable`] + The :class:`TextChannel` or :class:`Thread` that the message was sent from. + Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. + reference: Optional[:class:`~discord.MessageReference`] + The message that this message references. This is only applicable to messages of + type :attr:`MessageType.pins_add`, crossposted messages created by a + followed channel integration, or message replies. + + .. versionadded:: 1.5 + + mention_everyone: :class:`bool` + Specifies if the message mentions everyone. + + .. note:: + + This does not check if the ``@everyone`` or the ``@here`` text is in the message itself. + Rather this boolean indicates if either the ``@everyone`` or the ``@here`` text is in the message + **and** it did end up mentioning. + mentions: List[:class:`abc.User`] + A list of :class:`Member` that were mentioned. If the message is in a private message + then the list will be of :class:`User` instead. For messages that are not of type + :attr:`MessageType.default`\, this array can be used to aid in system messages. + For more information, see :attr:`system_content`. + + .. warning:: + + The order of the mentions list is not in any particular order so you should + not rely on it. This is a Discord limitation, not one with the library. + channel_mentions: List[Union[:class:`abc.GuildChannel`, :class:`Thread`]] + A list of :class:`abc.GuildChannel` or :class:`Thread` that were mentioned. If the message is + in a private message then the list is always empty. + role_mentions: List[:class:`Role`] + A list of :class:`Role` that were mentioned. If the message is in a private message + then the list is always empty. id: :class:`int` The message ID. + webhook_id: Optional[:class:`int`] + If this message was sent by a webhook, then this is the webhook ID's that sent this + message. + attachments: List[:class:`Attachment`] + A list of attachments given to a message. + pinned: :class:`bool` + Specifies if the message is currently pinned. + flags: :class:`MessageFlags` + Extra features of the message. + + .. versionadded:: 1.3 + + reactions : List[:class:`Reaction`] + Reactions to a message. Reactions can be either custom emoji or standard unicode emoji. + activity: Optional[:class:`dict`] + The activity associated with this message. Sent with Rich-Presence related messages that for + example, request joining, spectating, or listening to or with another member. + + It is a dictionary with the following optional keys: + + - ``type``: An integer denoting the type of message activity being requested. + - ``party_id``: The party ID associated with the party. + application: Optional[:class:`dict`] + The rich presence enabled application associated with this message. + + It is a dictionary with the following keys: + + - ``id``: A string representing the application's ID. + - ``name``: A string representing the application's name. + - ``description``: A string representing the application's description. + - ``icon``: A string representing the icon ID of the application. + - ``cover_image``: A string representing the embed's image asset ID. + stickers: List[:class:`StickerItem`] + A list of sticker items given to the message. + + .. versionadded:: 1.6 + components: List[:class:`Component`] + A list of components in the message. + + .. versionadded:: 2.0 + guild: Optional[:class:`Guild`] + The guild that the message belongs to, if applicable. """ - __slots__ = ('channel', 'id', '_cs_guild', '_state') + __slots__ = ( + '_state', + '_edited_timestamp', + '_cs_channel_mentions', + '_cs_raw_mentions', + '_cs_clean_content', + '_cs_raw_channel_mentions', + '_cs_raw_role_mentions', + '_cs_system_content', + 'tts', + 'content', + 'channel', + 'webhook_id', + 'mention_everyone', + 'embeds', + 'mentions', + 'author', + 'attachments', + 'nonce', + 'pinned', + 'role_mentions', + 'type', + 'flags', + 'reactions', + 'reference', + 'application', + 'activity', + 'stickers', + 'components', + ) - jump_url: str = Message.jump_url # type: ignore - edit = Message.edit - add_files = Message.add_files - remove_attachments = Message.remove_attachments - delete = Message.delete - publish = Message.publish - pin = Message.pin - unpin = Message.unpin - add_reaction = Message.add_reaction - remove_reaction = Message.remove_reaction - clear_reaction = Message.clear_reaction - clear_reactions = Message.clear_reactions - reply = Message.reply - to_reference = Message.to_reference - to_message_reference_dict = Message.to_message_reference_dict + if TYPE_CHECKING: + _HANDLERS: ClassVar[List[Tuple[str, Callable[..., None]]]] + _CACHED_SLOTS: ClassVar[List[str]] + # guild: Optional[Guild] + reference: Optional[MessageReference] + mentions: List[Union[User, Member]] + author: Union[User, Member] + role_mentions: List[Role] - def __init__(self, *, channel: PartialMessageableChannel, id: int): - if not isinstance(channel, PartialMessageable) and channel.type not in ( - ChannelType.text, - ChannelType.news, - ChannelType.private, - ChannelType.news_thread, - ChannelType.public_thread, - ChannelType.private_thread, - ): - raise TypeError(f'Expected PartialMessageable, TextChannel, DMChannel or Thread not {type(channel)!r}') + def __init__( + self, + *, + state: ConnectionState, + channel: MessageableChannel, + data: MessagePayload, + ) -> None: + super().__init__(channel=channel, id=int(data['id'])) + self._state: ConnectionState = state + self.webhook_id: Optional[int] = utils._get_as_snowflake(data, 'webhook_id') + self.reactions: List[Reaction] = [Reaction(message=self, data=d) for d in data.get('reactions', [])] + self.attachments: List[Attachment] = [Attachment(data=a, state=self._state) for a in data['attachments']] + self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']] + self.application: Optional[MessageApplicationPayload] = data.get('application') + self.activity: Optional[MessageActivityPayload] = data.get('activity') + self.channel: MessageableChannel = channel + self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp']) + self.type: MessageType = try_enum(MessageType, data['type']) + self.pinned: bool = data['pinned'] + self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0)) + self.mention_everyone: bool = data['mention_everyone'] + self.tts: bool = data['tts'] + self.content: str = data['content'] + self.nonce: Optional[Union[int, str]] = data.get('nonce') + 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.channel: PartialMessageableChannel = channel - self._state: ConnectionState = channel._state - self.id: int = id + try: + # if the channel doesn't have a guild attribute, we handle that + self.guild = channel.guild # type: ignore + except AttributeError: + self.guild = state._get_guild(utils._get_as_snowflake(data, 'guild_id')) - def _update(self, data) -> None: - # This is used for duck typing purposes. - # Just do nothing with the data. - pass + try: + ref = data['message_reference'] + except KeyError: + self.reference = None + else: + self.reference = ref = MessageReference.with_state(state, ref) + try: + resolved = data['referenced_message'] + except KeyError: + pass + else: + if resolved is None: + ref.resolved = DeletedReferencedMessage(ref) + else: + # Right now the channel IDs match but maybe in the future they won't. + if ref.channel_id == channel.id: + chan = channel + elif isinstance(channel, Thread) and channel.parent_id == ref.channel_id: + chan = channel + else: + chan, _ = state._get_guild_channel(resolved, ref.guild_id) - # Also needed for duck typing purposes - # n.b. not exposed - pinned: Any = property(None, lambda x, y: None) + # the channel will be the correct type here + ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore + + for handler in ('author', 'member', 'mentions', 'mention_roles'): + try: + getattr(self, f'_handle_{handler}')(data[handler]) + except KeyError: + continue def __repr__(self) -> str: - return f'' + name = self.__class__.__name__ + return ( + f'<{name} id={self.id} channel={self.channel!r} type={self.type!r} author={self.author!r} flags={self.flags!r}>' + ) + + def _try_patch(self, data, key, transform=None) -> None: + try: + value = data[key] + except KeyError: + pass + else: + if transform is None: + setattr(self, key, value) + else: + setattr(self, key, transform(value)) + + def _add_reaction(self, data, emoji, user_id) -> Reaction: + reaction = utils.find(lambda r: r.emoji == emoji, self.reactions) + is_me = data['me'] = user_id == self._state.self_id + + if reaction is None: + reaction = Reaction(message=self, data=data, emoji=emoji) + self.reactions.append(reaction) + else: + reaction.count += 1 + if is_me: + reaction.me = is_me + + return reaction + + def _remove_reaction(self, data: MessageReactionRemoveEvent, emoji: EmojiInputType, user_id: int) -> Reaction: + reaction = utils.find(lambda r: r.emoji == emoji, self.reactions) + + if reaction is None: + # already removed? + raise ValueError('Emoji already removed?') + + # if reaction isn't in the list, we crash. This means discord + # sent bad data, or we stored improperly + reaction.count -= 1 + + if user_id == self._state.self_id: + reaction.me = False + if reaction.count == 0: + # this raises ValueError if something went wrong as well. + self.reactions.remove(reaction) + + return reaction + + def _clear_emoji(self, emoji: PartialEmoji) -> Optional[Reaction]: + to_check = str(emoji) + for index, reaction in enumerate(self.reactions): + if str(reaction.emoji) == to_check: + break + else: + # didn't find anything so just return + return + + del self.reactions[index] + return reaction + + def _update(self, data: MessageUpdateEvent) -> None: + # In an update scheme, 'author' key has to be handled before 'member' + # otherwise they overwrite each other which is undesirable. + # Since there's no good way to do this we have to iterate over every + # handler rather than iterating over the keys which is a little slower + for key, handler in self._HANDLERS: + try: + value = data[key] + except KeyError: + continue + else: + handler(self, value) + + # clear the cached properties + for attr in self._CACHED_SLOTS: + try: + delattr(self, attr) + except AttributeError: + pass + + def _handle_edited_timestamp(self, value: str) -> None: + self._edited_timestamp = utils.parse_time(value) + + def _handle_pinned(self, value: bool) -> None: + self.pinned = value + + def _handle_flags(self, value: int) -> None: + self.flags = MessageFlags._from_value(value) + + def _handle_application(self, value: MessageApplicationPayload) -> None: + self.application = value + + def _handle_activity(self, value: MessageActivityPayload) -> None: + self.activity = value + + def _handle_mention_everyone(self, value: bool) -> None: + self.mention_everyone = value + + def _handle_tts(self, value: bool) -> None: + self.tts = value + + def _handle_type(self, value: int) -> None: + self.type = try_enum(MessageType, value) + + def _handle_content(self, value: str) -> None: + self.content = value + + def _handle_attachments(self, value: List[AttachmentPayload]) -> None: + self.attachments = [Attachment(data=a, state=self._state) for a in value] + + def _handle_embeds(self, value: List[EmbedPayload]) -> None: + self.embeds = [Embed.from_dict(data) for data in value] + + def _handle_nonce(self, value: Union[str, int]) -> None: + self.nonce = value + + def _handle_author(self, author: UserPayload) -> None: + self.author = self._state.store_user(author) + if isinstance(self.guild, Guild): + found = self.guild.get_member(self.author.id) + if found is not None: + self.author = found + + def _handle_member(self, member: MemberPayload) -> None: + # The gateway now gives us full Member objects sometimes with the following keys + # deaf, mute, joined_at, roles + # For the sake of performance I'm going to assume that the only + # field that needs *updating* would be the joined_at field. + # If there is no Member object (for some strange reason), then we can upgrade + # ourselves to a more "partial" member object. + author = self.author + try: + # Update member reference + author._update_from_message(member) # type: ignore + except AttributeError: + # It's a user here + # TODO: consider adding to cache here + self.author = Member._from_message(message=self, data=member) + + def _handle_mentions(self, mentions: List[UserWithMemberPayload]) -> None: + self.mentions = r = [] + guild = self.guild + state = self._state + if not isinstance(guild, Guild): + self.mentions = [state.store_user(m) for m in mentions] + return + + for mention in filter(None, mentions): + id_search = int(mention['id']) + member = guild.get_member(id_search) + if member is not None: + r.append(member) + else: + r.append(Member._try_upgrade(data=mention, guild=guild, state=state)) + + def _handle_mention_roles(self, role_mentions: List[int]) -> None: + self.role_mentions = [] + if isinstance(self.guild, Guild): + for role_id in map(int, role_mentions): + role = self.guild.get_role(role_id) + if role is not None: + self.role_mentions.append(role) + + def _handle_components(self, components: List[ComponentPayload]): + self.components = [_component_factory(d) for d in components] + + def _rebind_cached_references(self, new_guild: Guild, new_channel: Union[TextChannel, Thread]) -> None: + self.guild = new_guild + self.channel = new_channel + + @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_channel_mentions') + def channel_mentions(self) -> List[Union[GuildChannel, Thread]]: + if self.guild is None: + return [] + it = filter(None, map(self.guild._resolve_channel, self.raw_channel_mentions)) + return utils._unique(it) + + @utils.cached_slot_property('_cs_clean_content') + def clean_content(self) -> str: + """:class:`str`: A property that returns the content in a "cleaned up" + manner. This basically means that mentions are transformed + into the way the client shows it. e.g. ``<#id>`` will transform + into ``#name``. + + This will also transform @everyone and @here mentions into + non-mentions. + + .. note:: + + This *does not* affect markdown. If you want to escape + or remove markdown then use :func:`utils.escape_markdown` or :func:`utils.remove_markdown` + respectively, along with this function. + """ + + if self.guild: + + def resolve_member(id: int) -> str: + m = self.guild.get_member(id) or utils.get(self.mentions, id=id) # type: ignore + return f'@{m.display_name}' if m else '@deleted-user' + + def resolve_role(id: int) -> str: + r = self.guild.get_role(id) or utils.get(self.role_mentions, id=id) # type: ignore + return f'@{r.name}' if r else '@deleted-role' + + def resolve_channel(id: int) -> str: + c = self.guild._resolve_channel(id) # type: ignore + return f'#{c.name}' if c else '#deleted-channel' + + else: + + def resolve_member(id: int) -> str: + m = utils.get(self.mentions, id=id) + return f'@{m.display_name}' if m else '@deleted-user' + + def resolve_role(id: int) -> str: + return '@deleted-role' + + def resolve_channel(id: int) -> str: + return f'#deleted-channel' + + transforms = { + '@': resolve_member, + '@!': resolve_member, + '#': resolve_channel, + '@&': resolve_role, + } + + def repl(match: re.Match) -> str: + type = match[1] + id = int(match[2]) + transformed = transforms[type](id) + return transformed + + result = re.sub(r'<(@[!&]?|#)([0-9]{15,20})>', repl, self.content) + + return escape_mentions(result) @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: The partial message's creation time in UTC.""" + """:class:`datetime.datetime`: The message's creation time in UTC.""" return utils.snowflake_time(self.id) - @utils.cached_slot_property('_cs_guild') - def guild(self) -> Optional[Guild]: - """Optional[:class:`Guild`]: The guild that the partial message belongs to, if applicable.""" - return getattr(self.channel, 'guild', None) + @property + def edited_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the edited time of the message.""" + return self._edited_timestamp - async def fetch(self) -> Message: + def is_system(self) -> bool: + """:class:`bool`: Whether the message is a system message. + + A system message is a message that is constructed entirely by the Discord API + in response to something. + + .. versionadded:: 1.3 + """ + return self.type not in ( + MessageType.default, + MessageType.reply, + MessageType.application_command, + MessageType.thread_starter_message, + ) + + @utils.cached_slot_property('_cs_system_content') + def system_content(self) -> Optional[str]: + r""":class:`str`: A property that returns the content that is rendered + regardless of the :attr:`Message.type`. + + In the case of :attr:`MessageType.default` and :attr:`MessageType.reply`\, + this just returns the regular :attr:`Message.content`. Otherwise this + returns an English message denoting the contents of the system message. + """ + + if self.type is MessageType.default: + return self.content + + if self.type is MessageType.recipient_add: + if self.channel.type is ChannelType.group: + return f'{self.author.name} added {self.mentions[0].name} to the group.' + else: + return f'{self.author.name} added {self.mentions[0].name} to the thread.' + + if self.type is MessageType.recipient_remove: + if self.channel.type is ChannelType.group: + return f'{self.author.name} removed {self.mentions[0].name} from the group.' + else: + return f'{self.author.name} removed {self.mentions[0].name} from the thread.' + + if self.type is MessageType.channel_name_change: + return f'{self.author.name} changed the channel name: **{self.content}**' + + if self.type is MessageType.channel_icon_change: + return f'{self.author.name} changed the channel icon.' + + if self.type is MessageType.pins_add: + return f'{self.author.name} pinned a message to this channel.' + + if self.type is MessageType.new_member: + formats = [ + "{0} joined the party.", + "{0} is here.", + "Welcome, {0}. We hope you brought pizza.", + "A wild {0} appeared.", + "{0} just landed.", + "{0} just slid into the server.", + "{0} just showed up!", + "Welcome {0}. Say hi!", + "{0} hopped into the server.", + "Everyone welcome {0}!", + "Glad you're here, {0}.", + "Good to see you, {0}.", + "Yay you made it, {0}!", + ] + + created_at_ms = int(self.created_at.timestamp() * 1000) + return formats[created_at_ms % len(formats)].format(self.author.name) + + if self.type is MessageType.premium_guild_subscription: + if not self.content: + return f'{self.author.name} just boosted the server!' + else: + return f'{self.author.name} just boosted the server **{self.content}** times!' + + if self.type is MessageType.premium_guild_tier_1: + if not self.content: + return f'{self.author.name} just boosted the server! {self.guild} has achieved **Level 1!**' + else: + return f'{self.author.name} just boosted the server **{self.content}** times! {self.guild} has achieved **Level 1!**' + + if self.type is MessageType.premium_guild_tier_2: + if not self.content: + return f'{self.author.name} just boosted the server! {self.guild} has achieved **Level 2!**' + else: + return f'{self.author.name} just boosted the server **{self.content}** times! {self.guild} has achieved **Level 2!**' + + if self.type is MessageType.premium_guild_tier_3: + if not self.content: + return f'{self.author.name} just boosted the server! {self.guild} has achieved **Level 3!**' + else: + return f'{self.author.name} just boosted the server **{self.content}** times! {self.guild} has achieved **Level 3!**' + + if self.type is MessageType.channel_follow_add: + return f'{self.author.name} has added {self.content} to this channel' + + if self.type is MessageType.guild_stream: + # the author will be a Member + return f'{self.author.name} is live! Now streaming {self.author.activity.name}' # type: ignore + + if self.type is MessageType.guild_discovery_disqualified: + return 'This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details.' + + if self.type is MessageType.guild_discovery_requalified: + return 'This server is eligible for Server Discovery again and has been automatically relisted!' + + if self.type is MessageType.guild_discovery_grace_period_initial_warning: + return 'This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery.' + + if self.type is MessageType.guild_discovery_grace_period_final_warning: + return 'This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.' + + if self.type is MessageType.thread_created: + return f'{self.author.name} started a thread: **{self.content}**. See all **threads**.' + + if self.type is MessageType.reply: + return self.content + + if self.type is MessageType.thread_starter_message: + if self.reference is None or self.reference.resolved is None: + return 'Sorry, we couldn\'t load the first message in this thread' + + # the resolved message for the reference will be a Message + return self.reference.resolved.content # type: ignore + + if self.type is MessageType.guild_invite_reminder: + return 'Wondering who to invite?\nStart by inviting anyone who can help you build the server!' + + @overload + async def edit( + self, + *, + content: Optional[str] = ..., + embed: Optional[Embed] = ..., + attachments: Sequence[Union[Attachment, File]] = ..., + suppress: bool = ..., + delete_after: Optional[float] = ..., + allowed_mentions: Optional[AllowedMentions] = ..., + view: Optional[View] = ..., + ) -> Message: + ... + + @overload + async def edit( + self, + *, + content: Optional[str] = ..., + embeds: Sequence[Embed] = ..., + attachments: Sequence[Union[Attachment, File]] = ..., + suppress: bool = ..., + delete_after: Optional[float] = ..., + allowed_mentions: Optional[AllowedMentions] = ..., + view: Optional[View] = ..., + ) -> Message: + ... + + async def edit( + self, + content: Optional[str] = MISSING, + embed: Optional[Embed] = MISSING, + embeds: Sequence[Embed] = MISSING, + attachments: Sequence[Union[Attachment, File]] = MISSING, + suppress: bool = False, + delete_after: Optional[float] = None, + allowed_mentions: Optional[AllowedMentions] = MISSING, + view: Optional[View] = MISSING, + ) -> Message: """|coro| - Fetches the partial message to a full :class:`Message`. + Edits the message. + + The content must be able to be transformed into a string via ``str(content)``. + + .. versionchanged:: 1.3 + The ``suppress`` keyword-only parameter was added. + + .. versionchanged:: 2.0 + Edits are no longer in-place, the newly edited message is returned instead. + + .. versionchanged:: 2.0 + This function will now raise :exc:`TypeError` instead of + ``InvalidArgument``. + + Parameters + ----------- + content: Optional[:class:`str`] + The new content to replace the message with. + Could be ``None`` to remove the content. + embed: Optional[:class:`Embed`] + The new embed to replace the original with. + Could be ``None`` to remove the embed. + embeds: List[:class:`Embed`] + The new embeds to replace the original with. Must be a maximum of 10. + To remove all embeds ``[]`` should be passed. + + .. versionadded:: 2.0 + attachments: List[Union[:class:`Attachment`, :class:`File`]] + A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed + then all attachments are removed. + + .. note:: + + New files will always appear after current attachments. + + .. versionadded:: 2.0 + suppress: :class:`bool` + Whether to suppress embeds for the message. This removes + all the embeds if set to ``True``. If set to ``False`` + this brings the embeds back if they were suppressed. + Using this parameter requires :attr:`~.Permissions.manage_messages`. + delete_after: Optional[:class:`float`] + If provided, the number of seconds to wait in the background + before deleting the message we just edited. If the deletion fails, + then it is silently ignored. + allowed_mentions: Optional[:class:`~discord.AllowedMentions`] + Controls the mentions being processed in this message. If this is + passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`. + The merging behaviour only overrides attributes that have been explicitly passed + to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`. + If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions` + are used instead. + + .. versionadded:: 1.4 + view: Optional[:class:`~discord.ui.View`] + The updated view to update this message with. If ``None`` is passed then + the view is removed. Raises - -------- - NotFound - The message was not found. - Forbidden - You do not have the permissions required to get a message. + ------- HTTPException - Retrieving the message failed. + Editing the message failed. + Forbidden + Tried to suppress a message without permissions or + edited a message's content or embed that isn't yours. + TypeError + You specified both ``embed`` and ``embeds`` Returns -------- :class:`Message` - The full message. + The newly edited message. """ - data = await self._state.http.get_message(self.channel.id, self.id) - return self._state.create_message(channel=self.channel, data=data) + if content is not MISSING: + previous_allowed_mentions = self._state.allowed_mentions + else: + previous_allowed_mentions = None + + if suppress is not MISSING: + flags = MessageFlags._from_value(self.flags.value) + flags.suppress_embeds = suppress + else: + flags = MISSING + + if view is not MISSING: + self._state.prevent_view_updates_for(self.id) + + params = handle_message_parameters( + content=content, + flags=flags, + embed=embed, + embeds=embeds, + attachments=attachments, + view=view, + allowed_mentions=allowed_mentions, + previous_allowed_mentions=previous_allowed_mentions, + ) + data = await self._state.http.edit_message(self.channel.id, self.id, params=params) + message = Message(state=self._state, channel=self.channel, data=data) + + if view and not view.is_finished(): + self._state.store_view(view, self.id) + + if delete_after is not None: + await self.delete(delay=delete_after) + + return message + + async def add_files(self, *files: File) -> Message: + r"""|coro| + + Adds new files to the end of the message attachments. + + .. versionadded:: 2.0 + + Parameters + ----------- + \*files: :class:`File` + New files to add to the message. + + Raises + ------- + HTTPException + Editing the message failed. + Forbidden + Tried to edit a message that isn't yours. + + Returns + -------- + :class:`Message` + The newly edited message. + """ + return await self.edit(attachments=[*self.attachments, *files]) + + async def remove_attachments(self, *attachments: Attachment) -> Message: + r"""|coro| + + Removes attachments from the message. + + .. versionadded:: 2.0 + + Parameters + ----------- + \*attachments: :class:`Attachment` + Attachments to remove from the message. + + Raises + ------- + HTTPException + Editing the message failed. + Forbidden + Tried to edit a message that isn't yours. + + Returns + -------- + :class:`Message` + The newly edited message. + """ + return await self.edit(attachments=[a for a in self.attachments if a not in attachments]) diff --git a/docs/api.rst b/docs/api.rst index 09a2c7fe8..f9f33ad92 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3564,6 +3564,7 @@ Message .. autoclass:: Message() :members: + :inherited-members: DeletedReferencedMessage ~~~~~~~~~~~~~~~~~~~~~~~~~