Browse Source

Add support for PartialMessageable instances

This allows library users to send messages to channels without fetching
it first.
pull/7362/head
Rapptz 4 years ago
parent
commit
1279510194
  1. 4
      discord/abc.py
  2. 110
      discord/channel.py
  3. 22
      discord/client.py
  4. 6
      discord/message.py
  5. 4
      discord/state.py
  6. 9
      docs/api.rst

4
discord/abc.py

@ -78,7 +78,7 @@ if TYPE_CHECKING:
from .channel import CategoryChannel from .channel import CategoryChannel
from .embeds import Embed from .embeds import Embed
from .message import Message, MessageReference, PartialMessage from .message import Message, MessageReference, PartialMessage
from .channel import TextChannel, DMChannel, GroupChannel from .channel import TextChannel, DMChannel, GroupChannel, PartialMessageable
from .threads import Thread from .threads import Thread
from .enums import InviteTarget from .enums import InviteTarget
from .ui.view import View from .ui.view import View
@ -88,7 +88,7 @@ if TYPE_CHECKING:
OverwriteType, OverwriteType,
) )
PartialMessageableChannel = Union[TextChannel, Thread, DMChannel] PartialMessageableChannel = Union[TextChannel, Thread, DMChannel, PartialMessageable]
MessageableChannel = Union[PartialMessageableChannel, GroupChannel] MessageableChannel = Union[PartialMessageableChannel, GroupChannel]
SnowflakeTime = Union["Snowflake", datetime] SnowflakeTime = Union["Snowflake", datetime]

110
discord/channel.py

@ -26,13 +26,28 @@ from __future__ import annotations
import time import time
import asyncio 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 datetime
import discord.abc import discord.abc
from .permissions import PermissionOverwrite, Permissions from .permissions import PermissionOverwrite, Permissions
from .enums import ChannelType, StagePrivacyLevel, try_enum, VoiceRegion, VideoQualityMode from .enums import ChannelType, StagePrivacyLevel, try_enum, VoiceRegion, VideoQualityMode
from .mixins import Hashable from .mixins import Hashable
from .object import Object
from . import utils from . import utils
from .utils import MISSING from .utils import MISSING
from .asset import Asset from .asset import Asset
@ -49,6 +64,7 @@ __all__ = (
'CategoryChannel', 'CategoryChannel',
'StoreChannel', 'StoreChannel',
'GroupChannel', 'GroupChannel',
'PartialMessageable',
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@ -648,7 +664,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
message: Optional[Snowflake] = None, message: Optional[Snowflake] = None,
auto_archive_duration: ThreadArchiveDuration = 1440, auto_archive_duration: ThreadArchiveDuration = 1440,
type: Optional[ChannelType] = None, type: Optional[ChannelType] = None,
reason: Optional[str] = None reason: Optional[str] = None,
) -> Thread: ) -> Thread:
"""|coro| """|coro|
@ -1147,7 +1163,9 @@ class StageChannel(VocalGuildChannel):
""" """
return utils.get(self.guild.stage_instances, channel_id=self.id) 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| """|coro|
Create a stage instance. Create a stage instance.
@ -1651,9 +1669,6 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
await self._edit(options, reason=reason) await self._edit(options, reason=reason)
DMC = TypeVar('DMC', bound='DMChannel')
class DMChannel(discord.abc.Messageable, Hashable): class DMChannel(discord.abc.Messageable, Hashable):
"""Represents a Discord direct message channel. """Represents a Discord direct message channel.
@ -1677,10 +1692,8 @@ class DMChannel(discord.abc.Messageable, Hashable):
Attributes Attributes
---------- ----------
recipient: Optional[:class:`User`] recipient: :class:`User`
The user you are participating with in the direct message channel. 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` me: :class:`ClientUser`
The user presenting yourself. The user presenting yourself.
id: :class:`int` id: :class:`int`
@ -1691,7 +1704,7 @@ class DMChannel(discord.abc.Messageable, Hashable):
def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload): def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload):
self._state: ConnectionState = state 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.me: ClientUser = me
self.id: int = int(data['id']) self.id: int = int(data['id'])
@ -1699,22 +1712,11 @@ class DMChannel(discord.abc.Messageable, Hashable):
return self return self
def __str__(self) -> str: def __str__(self) -> str:
if self.recipient: return f'Direct Message with {self.recipient}'
return f'Direct Message with {self.recipient}'
return 'Direct Message with Unknown User'
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<DMChannel id={self.id} recipient={self.recipient!r}>' return f'<DMChannel id={self.id} recipient={self.recipient!r}>'
@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 @property
def type(self) -> ChannelType: def type(self) -> ChannelType:
""":class:`ChannelType`: The channel's Discord type.""" """: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) 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): def _guild_channel_factory(channel_type: int):
value = try_enum(ChannelType, channel_type) value = try_enum(ChannelType, channel_type)
if value is ChannelType.text: if value is ChannelType.text:
@ -1949,6 +2014,7 @@ def _channel_factory(channel_type: int):
else: else:
return cls, value return cls, value
def _threaded_channel_factory(channel_type: int): def _threaded_channel_factory(channel_type: int):
cls, value = _channel_factory(channel_type) cls, value = _channel_factory(channel_type)
if value in (ChannelType.private_thread, ChannelType.public_thread, ChannelType.news_thread): if value in (ChannelType.private_thread, ChannelType.public_thread, ChannelType.news_thread):

22
discord/client.py

@ -39,7 +39,7 @@ from .template import Template
from .widget import Widget from .widget import Widget
from .guild import Guild from .guild import Guild
from .emoji import Emoji from .emoji import Emoji
from .channel import _threaded_channel_factory from .channel import _threaded_channel_factory, PartialMessageable
from .enums import ChannelType from .enums import ChannelType
from .mentions import AllowedMentions from .mentions import AllowedMentions
from .errors import * from .errors import *
@ -729,6 +729,26 @@ class Client:
""" """
return self._connection.get_channel(id) 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]: def get_stage_instance(self, id) -> Optional[StageInstance]:
"""Returns a stage instance with the given stage channel ID. """Returns a stage instance with the given stage channel ID.

6
discord/message.py

@ -70,7 +70,7 @@ if TYPE_CHECKING:
from .abc import GuildChannel, PartialMessageableChannel, MessageableChannel from .abc import GuildChannel, PartialMessageableChannel, MessageableChannel
from .components import Component from .components import Component
from .state import ConnectionState from .state import ConnectionState
from .channel import TextChannel, GroupChannel, DMChannel from .channel import TextChannel, GroupChannel, DMChannel, PartialMessageable
from .mentions import AllowedMentions from .mentions import AllowedMentions
from .user import User from .user import User
from .role import Role 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. This is not stored long term within Discord's servers and is only used ephemerally.
embeds: List[:class:`Embed`] embeds: List[:class:`Embed`]
A list of embeds the message has. 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. 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. Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message.
reference: Optional[:class:`~discord.MessageReference`] reference: Optional[:class:`~discord.MessageReference`]
@ -646,7 +646,7 @@ class Message(Hashable):
self, self,
*, *,
state: ConnectionState, state: ConnectionState,
channel: Union[TextChannel, Thread, DMChannel, GroupChannel], channel: Union[TextChannel, Thread, DMChannel, GroupChannel, PartialMessageable],
data: MessagePayload, data: MessagePayload,
): ):
self._state: ConnectionState = state self._state: ConnectionState = state

4
discord/state.py

@ -405,12 +405,12 @@ class ConnectionState:
try: try:
guild = self._get_guild(int(data['guild_id'])) guild = self._get_guild(int(data['guild_id']))
except KeyError: except KeyError:
channel = DMChannel._from_message(self, channel_id) channel = PartialMessageable(state=self, id=channel_id, type=ChannelType.private)
guild = None guild = None
else: else:
channel = guild and guild._resolve_channel(channel_id) 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): async def chunker(self, guild_id, query='', limit=0, presences=False, *, nonce=None):
ws = self._get_websocket(guild_id) # This is ignored upstream ws = self._get_websocket(guild_id) # This is ignored upstream

9
docs/api.rst

@ -3574,6 +3574,15 @@ RoleTags
.. autoclass:: RoleTags() .. autoclass:: RoleTags()
:members: :members:
PartialMessageable
~~~~~~~~~~~~~~~~~~~~
.. attributetable:: PartialMessageable
.. autoclass:: PartialMessageable()
:members:
:inherited-members:
TextChannel TextChannel
~~~~~~~~~~~~ ~~~~~~~~~~~~

Loading…
Cancel
Save