diff --git a/discord/abc.py b/discord/abc.py index 0c077dae8..54f776c2b 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -78,7 +78,7 @@ if TYPE_CHECKING: from .channel import CategoryChannel from .embeds import Embed from .message import Message, MessageReference, PartialMessage - from .channel import TextChannel, DMChannel, GroupChannel + from .channel import TextChannel, DMChannel, GroupChannel, PartialMessageable from .threads import Thread from .enums import InviteTarget from .ui.view import View @@ -88,7 +88,7 @@ if TYPE_CHECKING: OverwriteType, ) - PartialMessageableChannel = Union[TextChannel, Thread, DMChannel] + PartialMessageableChannel = Union[TextChannel, Thread, DMChannel, PartialMessageable] MessageableChannel = Union[PartialMessageableChannel, GroupChannel] SnowflakeTime = Union["Snowflake", datetime] diff --git a/discord/channel.py b/discord/channel.py index 21b87c1b6..6d0fe0813 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -26,13 +26,28 @@ from __future__ import annotations import time import asyncio -from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union, overload +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Mapping, + Optional, + TYPE_CHECKING, + Tuple, + Type, + TypeVar, + Union, + overload, +) import datetime import discord.abc from .permissions import PermissionOverwrite, Permissions from .enums import ChannelType, StagePrivacyLevel, try_enum, VoiceRegion, VideoQualityMode from .mixins import Hashable +from .object import Object from . import utils from .utils import MISSING from .asset import Asset @@ -49,6 +64,7 @@ __all__ = ( 'CategoryChannel', 'StoreChannel', 'GroupChannel', + 'PartialMessageable', ) if TYPE_CHECKING: @@ -648,7 +664,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): message: Optional[Snowflake] = None, auto_archive_duration: ThreadArchiveDuration = 1440, type: Optional[ChannelType] = None, - reason: Optional[str] = None + reason: Optional[str] = None, ) -> Thread: """|coro| @@ -1147,7 +1163,9 @@ class StageChannel(VocalGuildChannel): """ return utils.get(self.guild.stage_instances, channel_id=self.id) - async def create_instance(self, *, topic: str, privacy_level: StagePrivacyLevel = MISSING, reason: Optional[str] = None) -> StageInstance: + async def create_instance( + self, *, topic: str, privacy_level: StagePrivacyLevel = MISSING, reason: Optional[str] = None + ) -> StageInstance: """|coro| Create a stage instance. @@ -1651,9 +1669,6 @@ class StoreChannel(discord.abc.GuildChannel, Hashable): await self._edit(options, reason=reason) -DMC = TypeVar('DMC', bound='DMChannel') - - class DMChannel(discord.abc.Messageable, Hashable): """Represents a Discord direct message channel. @@ -1677,10 +1692,8 @@ class DMChannel(discord.abc.Messageable, Hashable): Attributes ---------- - recipient: Optional[:class:`User`] + recipient: :class:`User` The user you are participating with in the direct message channel. - If this channel is received through the gateway, the recipient information - may not be always available. me: :class:`ClientUser` The user presenting yourself. id: :class:`int` @@ -1691,7 +1704,7 @@ class DMChannel(discord.abc.Messageable, Hashable): def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload): self._state: ConnectionState = state - self.recipient: Optional[User] = state.store_user(data['recipients'][0]) + self.recipient: User = state.store_user(data['recipients'][0]) self.me: ClientUser = me self.id: int = int(data['id']) @@ -1699,22 +1712,11 @@ class DMChannel(discord.abc.Messageable, Hashable): return self def __str__(self) -> str: - if self.recipient: - return f'Direct Message with {self.recipient}' - return 'Direct Message with Unknown User' + return f'Direct Message with {self.recipient}' def __repr__(self) -> str: return f'' - @classmethod - def _from_message(cls: Type[DMC], state: ConnectionState, channel_id: int) -> DMC: - self: DMC = cls.__new__(cls) - self._state = state - self.id = channel_id - self.recipient = None - self.me = state.user # type: ignore - return self - @property def type(self) -> ChannelType: """:class:`ChannelType`: The channel's Discord type.""" @@ -1922,6 +1924,69 @@ class GroupChannel(discord.abc.Messageable, Hashable): await self._state.http.leave_group(self.id) +class PartialMessageable(discord.abc.Messageable, Hashable): + """Represents a partial messageable to aid with working messageable channels when + only a channel ID are present. + + The only way to construct this class is through :meth:`Client.get_partial_messageable`. + + Note that this class is trimmed down and has no rich attributes. + + .. versionadded:: 2.0 + + .. container:: operations + + .. describe:: x == y + + Checks if two partial messageables are equal. + + .. describe:: x != y + + Checks if two partial messageables are not equal. + + .. describe:: hash(x) + + Returns the partial messageable's hash. + + Attributes + ----------- + id: :class:`int` + The channel ID associated with this partial messageable. + type: Optional[:class:`ChannelType`] + The channel type associated with this partial messageable, if given. + """ + + def __init__(self, state: ConnectionState, id: int, type: Optional[ChannelType] = None): + self._state: ConnectionState = state + self._channel: Object = Object(id=id) + self.id: int = id + self.type: Optional[ChannelType] = type + + async def _get_channel(self) -> Object: + return self._channel + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + Parameters + ------------ + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + --------- + :class:`PartialMessage` + The partial message. + """ + + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + def _guild_channel_factory(channel_type: int): value = try_enum(ChannelType, channel_type) if value is ChannelType.text: @@ -1949,6 +2014,7 @@ def _channel_factory(channel_type: int): else: return cls, value + def _threaded_channel_factory(channel_type: int): cls, value = _channel_factory(channel_type) if value in (ChannelType.private_thread, ChannelType.public_thread, ChannelType.news_thread): diff --git a/discord/client.py b/discord/client.py index 81c86d7f1..cdfc6bdbc 100644 --- a/discord/client.py +++ b/discord/client.py @@ -39,7 +39,7 @@ from .template import Template from .widget import Widget from .guild import Guild from .emoji import Emoji -from .channel import _threaded_channel_factory +from .channel import _threaded_channel_factory, PartialMessageable from .enums import ChannelType from .mentions import AllowedMentions from .errors import * @@ -729,6 +729,26 @@ class Client: """ return self._connection.get_channel(id) + def get_partial_messageable(self, id: int, *, type: Optional[ChannelType] = None) -> PartialMessageable: + """Returns a partial messageable with the given channel ID. + + This is useful if you have a channel_id but don't want to do an API call + to send messages to it. + + Parameters + ----------- + id: :class:`int` + The channel ID to create a partial messageable for. + type: Optional[:class:`ChannelType`] + The underlying channel type for the partial messageable. + + Returns + -------- + :class:`PartialMessageable` + The partial messageable + """ + return PartialMessageable(state=self._connection, id=id, type=type) + def get_stage_instance(self, id) -> Optional[StageInstance]: """Returns a stage instance with the given stage channel ID. diff --git a/discord/message.py b/discord/message.py index 270ac9e9e..d752c8c71 100644 --- a/discord/message.py +++ b/discord/message.py @@ -70,7 +70,7 @@ if TYPE_CHECKING: from .abc import GuildChannel, PartialMessageableChannel, MessageableChannel from .components import Component from .state import ConnectionState - from .channel import TextChannel, GroupChannel, DMChannel + from .channel import TextChannel, GroupChannel, DMChannel, PartialMessageable from .mentions import AllowedMentions from .user import User from .role import Role @@ -520,7 +520,7 @@ class Message(Hashable): 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`] + 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`] @@ -646,7 +646,7 @@ class Message(Hashable): self, *, state: ConnectionState, - channel: Union[TextChannel, Thread, DMChannel, GroupChannel], + channel: Union[TextChannel, Thread, DMChannel, GroupChannel, PartialMessageable], data: MessagePayload, ): self._state: ConnectionState = state diff --git a/discord/state.py b/discord/state.py index ca33df3d0..4a09a9cc0 100644 --- a/discord/state.py +++ b/discord/state.py @@ -405,12 +405,12 @@ class ConnectionState: try: guild = self._get_guild(int(data['guild_id'])) except KeyError: - channel = DMChannel._from_message(self, channel_id) + channel = PartialMessageable(state=self, id=channel_id, type=ChannelType.private) guild = None else: channel = guild and guild._resolve_channel(channel_id) - return channel or Object(id=channel_id), guild + return channel or PartialMessageable(state=self, id=channel_id), guild async def chunker(self, guild_id, query='', limit=0, presences=False, *, nonce=None): ws = self._get_websocket(guild_id) # This is ignored upstream diff --git a/docs/api.rst b/docs/api.rst index c8a09d881..82978e7c6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3574,6 +3574,15 @@ RoleTags .. autoclass:: RoleTags() :members: +PartialMessageable +~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: PartialMessageable + +.. autoclass:: PartialMessageable() + :members: + :inherited-members: + TextChannel ~~~~~~~~~~~~