aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRapptz <[email protected]>2021-04-14 05:05:47 -0400
committerRapptz <[email protected]>2021-06-08 07:23:40 -0400
commit68c7c538f5a4e068664c3ee2f38f7343e229af1a (patch)
tree08a694d1f32e27283e08302ab1388d8579a1bb0c
parent[types] Add support thread API typings (diff)
downloaddiscord.py-68c7c538f5a4e068664c3ee2f38f7343e229af1a.tar.xz
discord.py-68c7c538f5a4e068664c3ee2f38f7343e229af1a.zip
First pass at preliminary thread support
This is missing a lot of functionality right now, such as two gateway events and all the HTTP CRUD endpoints.
-rw-r--r--discord/__init__.py1
-rw-r--r--discord/channel.py8
-rw-r--r--discord/enums.py20
-rw-r--r--discord/flags.py7
-rw-r--r--discord/guild.py42
-rw-r--r--discord/http.py77
-rw-r--r--discord/state.py41
-rw-r--r--discord/threads.py244
-rw-r--r--docs/api.rst76
9 files changed, 504 insertions, 12 deletions
diff --git a/discord/__init__.py b/discord/__init__.py
index dcf421ff..b2c5a078 100644
--- a/discord/__init__.py
+++ b/discord/__init__.py
@@ -58,6 +58,7 @@ from .sticker import *
from .stage_instance import *
from .interactions import *
from .components import *
+from .threads import *
VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial')
diff --git a/discord/channel.py b/discord/channel.py
index 743619f4..5179c8c4 100644
--- a/discord/channel.py
+++ b/discord/channel.py
@@ -173,6 +173,14 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
"""List[:class:`Member`]: Returns all members that can see this channel."""
return [m for m in self.guild.members if self.permissions_for(m).read_messages]
+ @property
+ def threads(self):
+ """List[:class:`Thread`]: Returns all the threads that you can see.
+
+ .. versionadded:: 2.0
+ """
+ return [thread for thread in self.guild.threads if thread.parent_id == self.id]
+
def is_nsfw(self):
""":class:`bool`: Checks if the channel is NSFW."""
return self.nsfw
diff --git a/discord/enums.py b/discord/enums.py
index d716fa6f..c145c829 100644
--- a/discord/enums.py
+++ b/discord/enums.py
@@ -155,14 +155,16 @@ else:
return value
class ChannelType(Enum):
- text = 0
- private = 1
- voice = 2
- group = 3
- category = 4
- news = 5
- store = 6
- stage_voice = 13
+ text = 0
+ private = 1
+ voice = 2
+ group = 3
+ category = 4
+ news = 5
+ store = 6
+ public_thread = 11
+ private_thread = 12
+ stage_voice = 13
def __str__(self):
return self.name
@@ -186,8 +188,10 @@ class MessageType(Enum):
guild_discovery_requalified = 15
guild_discovery_grace_period_initial_warning = 16
guild_discovery_grace_period_final_warning = 17
+ thread_created = 18
reply = 19
application_command = 20
+ thread_starter_message = 21
guild_invite_reminder = 22
class VoiceRegion(Enum):
diff --git a/discord/flags.py b/discord/flags.py
index d6a892c8..0ca6e34d 100644
--- a/discord/flags.py
+++ b/discord/flags.py
@@ -279,6 +279,13 @@ class MessageFlags(BaseFlags):
"""
return 16
+ @flag_value
+ def has_thread(self):
+ """:class:`bool`: Returns ``True`` if the source message is associated with a thread.
+
+ .. versionadded:: 2.0
+ """
+ return 32
@fill_with_flags()
class PublicUserFlags(BaseFlags):
diff --git a/discord/guild.py b/discord/guild.py
index e9756905..c373cb52 100644
--- a/discord/guild.py
+++ b/discord/guild.py
@@ -47,6 +47,7 @@ from .asset import Asset
from .flags import SystemChannelFlags
from .integrations import Integration, _integration_factory
from .stage_instance import StageInstance
+from .threads import Thread
__all__ = (
'Guild',
@@ -182,7 +183,7 @@ class Guild(Hashable):
'description', 'max_presences', 'max_members', 'max_video_channel_users',
'premium_tier', 'premium_subscription_count', '_system_channel_flags',
'preferred_locale', '_discovery_splash', '_rules_channel_id',
- '_public_updates_channel_id', '_stage_instances', 'nsfw_level')
+ '_public_updates_channel_id', '_stage_instances', 'nsfw_level', '_threads')
_PREMIUM_GUILD_LIMITS = {
None: _GuildLimit(emoji=50, bitrate=96e3, filesize=8388608),
@@ -196,6 +197,7 @@ class Guild(Hashable):
self._channels = {}
self._members = {}
self._voice_states = {}
+ self._threads = {}
self._state = state
self._from_data(data)
@@ -214,6 +216,12 @@ class Guild(Hashable):
def _remove_member(self, member):
self._members.pop(member.id, None)
+ def _add_thread(self, thread):
+ self._threads[thread.id] = thread
+
+ def _remove_thread(self, thread):
+ self._threads.pop(thread.id, None)
+
def __str__(self):
return self.name or ''
@@ -360,12 +368,25 @@ class Guild(Hashable):
if factory:
self._add_channel(factory(guild=self, data=c, state=self._state))
+ if 'threads' in data:
+ threads = data['threads']
+ for thread in threads:
+ self._add_thread(Thread(guild=self, data=thread))
+
@property
def channels(self):
"""List[:class:`abc.GuildChannel`]: A list of channels that belongs to this guild."""
return list(self._channels.values())
@property
+ def threads(self):
+ """List[:class:`Thread`]: A list of threads that you have permission to view.
+
+ .. versionadded:: 2.0
+ """
+ return list(self._threads.values())
+
+ @property
def large(self):
""":class:`bool`: Indicates if the guild is a 'large' guild.
@@ -484,6 +505,23 @@ class Guild(Hashable):
"""
return self._channels.get(channel_id)
+ def get_thread(self, thread_id):
+ """Returns a thread with the given ID.
+
+ .. versionadded:: 2.0
+
+ Parameters
+ -----------
+ thread_id: :class:`int`
+ The ID to search for.
+
+ Returns
+ --------
+ Optional[:class:`Thread`]
+ The returned thread or ``None`` if not found.
+ """
+ return self._threads.get(thread_id)
+
@property
def system_channel(self):
"""Optional[:class:`TextChannel`]: Returns the guild's channel used for system messages.
@@ -2377,7 +2415,7 @@ class Guild(Hashable):
data = await self._state.http.get_widget(self.id)
return Widget(state=self._state, data=data)
-
+
async def edit_widget(self, *, enabled: bool = utils.MISSING, channel: Optional[abc.Snowflake] = utils.MISSING) -> None:
"""|coro|
diff --git a/discord/http.py b/discord/http.py
index 3c4d8400..ee93d1a9 100644
--- a/discord/http.py
+++ b/discord/http.py
@@ -695,6 +695,8 @@ class HTTPClient:
'type',
'rtc_region',
'video_quality_mode',
+ 'archived',
+ 'auto_archive_duration',
)
payload = {k: v for k, v in options.items() if k in valid_keys}
return self.request(r, reason=reason, json=payload)
@@ -720,6 +722,7 @@ class HTTPClient:
'rate_limit_per_user',
'rtc_region',
'video_quality_mode',
+ 'auto_archive_duration',
)
payload.update({k: v for k, v in options.items() if k in valid_keys and v is not None})
@@ -728,6 +731,80 @@ class HTTPClient:
def delete_channel(self, channel_id, *, reason=None):
return self.request(Route('DELETE', '/channels/{channel_id}', channel_id=channel_id), reason=reason)
+ # Thread management
+
+ def start_public_thread(
+ self,
+ channel_id: int,
+ message_id: int,
+ *,
+ name: str,
+ auto_archive_duration: int,
+ type: int,
+ ):
+ payload = {
+ 'name': name,
+ 'auto_archive_duration': auto_archive_duration,
+ 'type': type,
+ }
+
+ route = Route(
+ 'POST', '/channels/{channel_id}/messages/{message_id}/threads', channel_id=channel_id, message_id=message_id
+ )
+ return self.request(route, json=payload)
+
+ def start_private_thread(
+ self,
+ channel_id: int,
+ *,
+ name: str,
+ auto_archive_duration: int,
+ type: int,
+ ):
+ payload = {
+ 'name': name,
+ 'auto_archive_duration': auto_archive_duration,
+ 'type': type,
+ }
+
+ route = Route('POST', '/channels/{channel_id}/threads', channel_id=channel_id)
+ return self.request(route, json=payload)
+
+ def join_thread(self, channel_id: int):
+ return self.request(Route('POST', '/channels/{channel_id}/thread-members/@me', channel_id=channel_id))
+
+ def add_user_to_thread(self, channel_id: int, user_id: int):
+ return self.request(
+ Route('POST', '/channels/{channel_id}/thread-members/{user_id}', channel_id=channel_id, user_id=user_id)
+ )
+
+ def leave_thread(self, channel_id: int):
+ return self.request(Route('DELETE', '/channels/{channel_id}/thread-members/@me', channel_id=channel_id))
+
+ def remove_user_from_thread(self, channel_id: int, user_id: int):
+ route = Route('DELETE', '/channels/{channel_id}/thread-members/{user_id}', channel_id=channel_id, user_id=user_id)
+ return self.request(route)
+
+ def get_archived_threads(self, channel_id: int, before=None, limit: int = 50, public: bool = True):
+ if public:
+ route = Route('GET', '/channels/{channel_id}/threads/archived/public', channel_id=channel_id)
+ else:
+ route = Route('GET', '/channels/{channel_id}/threads/archived/private', channel_id=channel_id)
+
+ params = {}
+ if before:
+ params['before'] = before
+ params['limit'] = limit
+ return self.request(route, params=params)
+
+ def get_joined_private_archived_threads(self, channel_id, before=None, limit: int = 50):
+ route = Route('GET', '/channels/{channel_id}/users/@me/threads/archived/private', channel_id=channel_id)
+ params = {}
+ if before:
+ params['before'] = before
+ params['limit'] = limit
+ return self.request(route, params=params)
+
# Webhook management
def create_webhook(self, channel_id, *, name, avatar=None, reason=None):
diff --git a/discord/state.py b/discord/state.py
index f9c02856..7c1183d7 100644
--- a/discord/state.py
+++ b/discord/state.py
@@ -55,6 +55,7 @@ from .integrations import _integration_factory
from .interactions import Interaction
from .ui.view import ViewStore
from .stage_instance import StageInstance
+from .threads import Thread, ThreadMember
class ChunkRequest:
def __init__(self, guild_id, loop, resolver, *, cache=True):
@@ -483,7 +484,7 @@ class ConnectionState:
self.dispatch('message', message)
if self._messages is not None:
self._messages.append(message)
- if channel and channel.__class__ is TextChannel:
+ if channel and channel.__class__ in (TextChannel, Thread):
channel.last_message_id = message.id
def parse_message_delete(self, data):
@@ -704,6 +705,44 @@ class ConnectionState:
else:
self.dispatch('guild_channel_pins_update', channel, last_pin)
+ def parse_thread_create(self, data):
+ guild_id = int(data['guild_id'])
+ guild = self._get_guild(guild_id)
+ if guild is None:
+ log.debug('THREAD_CREATE referencing an unknown guild ID: %s. Discarding', guild_id)
+ return
+
+ thread = Thread(guild=guild, data=data)
+ guild._add_thread(thread)
+ self.dispatch('thread_create', thread)
+
+ def parse_thread_update(self, data):
+ guild_id = int(data['guild_id'])
+ guild = self._get_guild(guild_id)
+ if guild is None:
+ log.debug('THREAD_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
+ return
+
+ thread_id = int(data['id'])
+ thread = guild._get_thread(thread_id)
+ if thread is not None:
+ old = copy.copy(thread)
+ thread._update(data)
+ self.dispatch('thread_update', old, thread)
+
+ def parse_thread_delete(self, data):
+ guild_id = int(data['guild_id'])
+ guild = self._get_guild(guild_id)
+ if guild is None:
+ log.debug('THREAD_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
+ return
+
+ thread_id = int(data['id'])
+ thread = guild._get_thread(thread_id)
+ if thread is not None:
+ guild._remove_thread(thread)
+ self.dispatch('thread_delete', thread)
+
def parse_guild_member_add(self, data):
guild = self._get_guild(int(data['guild_id']))
if guild is None:
diff --git a/discord/threads.py b/discord/threads.py
new file mode 100644
index 00000000..cde4a329
--- /dev/null
+++ b/discord/threads.py
@@ -0,0 +1,244 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2015-present Rapptz
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+"""
+
+from __future__ import annotations
+from typing import Optional, TYPE_CHECKING
+
+from .mixins import Hashable
+from .abc import Messageable
+from .enums import ChannelType, try_enum
+from . import utils
+
+__all__ = (
+ 'Thread',
+ 'ThreadMember',
+)
+
+if TYPE_CHECKING:
+ from .types.threads import (
+ Thread as ThreadPayload,
+ ThreadMember as ThreadMemberPayload,
+ ThreadMetadata,
+ )
+ from .guild import Guild
+ from .channel import TextChannel
+ from .member import Member
+
+
+class Thread(Messageable, Hashable):
+ """Represents a Discord thread.
+
+ .. container:: operations
+
+ .. describe:: x == y
+
+ Checks if two threads are equal.
+
+ .. describe:: x != y
+
+ Checks if two threads are not equal.
+
+ .. describe:: hash(x)
+
+ Returns the thread's hash.
+
+ .. describe:: str(x)
+
+ Returns the thread's name.
+
+ .. versionadded:: 2.0
+
+ Attributes
+ -----------
+ name: :class:`str`
+ The thread name.
+ guild: :class:`Guild`
+ The guild the thread belongs to.
+ id: :class:`int`
+ The thread ID.
+ parent_id: :class:`int`
+ The parent :class:`TextChannel` ID this thread belongs to.
+ owner_id: :class:`int`
+ The user's ID that created this thread.
+ last_message_id: Optional[:class:`int`]
+ The last message ID of the message sent to this thread. It may
+ *not* point to an existing or valid message.
+ message_count: :class:`int`
+ An approximate number of messages in this thread. This caps at 50.
+ member_count: :class:`int`
+ An approximate number of members in this thread. This caps at 50.
+ me: Optional[:class:`ThreadMember`]
+ A thread member representing yourself, if you've joined the thread.
+ This could not be available.
+ archived: :class:`bool`
+ Whether the thread is archived.
+ archiver_id: Optional[:class:`int`]
+ The user's ID that archived this thread.
+ auto_archive_duration: :class:`int`
+ The duration in minutes until the thread is automatically archived due to inactivity.
+ Usually a value of 60, 1440, 4320 and 10080.
+ archive_timestamp: :class:`datetime.datetime`
+ An aware timestamp of when the thread's archived status was last updated in UTC.
+ """
+
+ __slots__ = (
+ 'name',
+ 'id',
+ 'guild',
+ '_type',
+ '_state',
+ 'owner_id',
+ 'last_message_id',
+ 'message_count',
+ 'member_count',
+ 'me',
+ 'archived',
+ 'archiver_id',
+ 'auto_archive_duration',
+ 'archive_timestamp',
+ )
+
+ def __init__(self, *, guild: Guild, data: ThreadPayload):
+ self._state = guild._state
+ self.guild = guild
+ self._from_data(data)
+
+ async def _get_channel(self):
+ return self
+
+ def _from_data(self, data: ThreadPayload):
+ self.id = int(data['id'])
+ self.parent_id = int(data['parent_id'])
+ self.owner_id = int(data['owner_id'])
+ self.name = data['name']
+ self.type = try_enum(ChannelType, data['type'])
+ self.last_message_id = utils._get_as_snowflake(data, 'last_message_id')
+ self._unroll_metadata(data['thread_metadata'])
+
+ try:
+ member = data['member']
+ except KeyError:
+ self.me = None
+ else:
+ self.me = ThreadMember(member, self._state)
+
+ def _unroll_metadata(self, data: ThreadMetadata):
+ self.archived = data['archived']
+ self.archiver_id = utils._get_as_snowflake(data, 'archiver_id')
+ self.auto_archive_duration = data['auto_archive_duration']
+ self.archive_timestamp = utils.parse_time(data['archive_timestamp'])
+
+ def _update(self, data):
+ try:
+ self.name = data['name']
+ except KeyError:
+ pass
+
+ try:
+ self._unroll_metadata(data['thread_metadata'])
+ except KeyError:
+ pass
+
+ @property
+ def parent(self) -> Optional[TextChannel]:
+ """Optional[:class:`TextChannel`]: The parent channel this thread belongs to."""
+ return self.guild.get_channel(self.parent_id)
+
+ @property
+ def owner(self) -> Optional[Member]:
+ """Optional[:class:`Member`]: The member this thread belongs to."""
+ return self.guild.get_member(self.owner_id)
+
+ @property
+ def last_message(self):
+ """Fetches the last message from this channel in cache.
+
+ The message might not be valid or point to an existing message.
+
+ .. admonition:: Reliable Fetching
+ :class: helpful
+
+ For a slightly more reliable method of fetching the
+ last message, consider using either :meth:`history`
+ or :meth:`fetch_message` with the :attr:`last_message_id`
+ attribute.
+
+ Returns
+ ---------
+ Optional[:class:`Message`]
+ The last message in this channel or ``None`` if not found.
+ """
+ return self._state._get_message(self.last_message_id) if self.last_message_id else None
+
+
+class ThreadMember(Hashable):
+ """Represents a Discord thread member.
+
+ .. container:: operations
+
+ .. describe:: x == y
+
+ Checks if two thread members are equal.
+
+ .. describe:: x != y
+
+ Checks if two thread members are not equal.
+
+ .. describe:: hash(x)
+
+ Returns the thread member's hash.
+
+ .. describe:: str(x)
+
+ Returns the thread member's name.
+
+ .. versionadded:: 2.0
+
+ Attributes
+ -----------
+ id: :class:`int`
+ The thread member's ID.
+ thread_id: :class:`int`
+ The thread's ID.
+ joined_at: :class:`datetime.datetime`
+ The time the member joined the thread in UTC.
+ """
+
+ __slots__ = (
+ 'id',
+ 'thread_id',
+ 'joined_at',
+ 'flags',
+ '_state',
+ )
+
+ def __init__(self, data: ThreadMemberPayload, state):
+ self._state = state
+ self._from_data(data)
+
+ def _from_data(self, data: ThreadMemberPayload):
+ self.id = int(data['user_id'])
+ self.thread_id = int(data['id'])
+ self.joined_at = utils.parse_time(data['join_timestamp'])
+ self.flags = data['flags']
diff --git a/docs/api.rst b/docs/api.rst
index 68b055f4..c0c60d8c 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -658,6 +658,33 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param last_pin: The latest message that was pinned as an aware datetime in UTC. Could be ``None``.
:type last_pin: Optional[:class:`datetime.datetime`]
+.. function:: on_thread_delete(thread)
+ on_thread_create(thread)
+
+ Called whenever a thread is deleted or created.
+
+ Note that you can get the guild from :attr:`Thread.guild`.
+
+ This requires :attr:`Intents.guilds` to be enabled.
+
+ .. versionadded:: 2.0
+
+ :param thread: The thread that got created or deleted.
+ :type thread: :class:`Thread`
+
+.. function:: on_thread_update(before, after)
+
+ Called whenever a thread is updated.
+
+ This requires :attr:`Intents.guilds` to be enabled.
+
+ .. versionadded:: 2.0
+
+ :param before: The updated thread's old info.
+ :type before: :class:`Thread`
+ :param after: The updated thread's new info.
+ :type after: :class:`Thread`
+
.. function:: on_guild_integrations_update(guild)
Called whenever an integration is created, modified, or removed from a guild.
@@ -1038,6 +1065,18 @@ of :class:`enum.Enum`.
.. versionadded:: 1.7
+ .. attribute:: public_thread
+
+ A public thread
+
+ .. versionadded:: 2.0
+
+ .. attribute:: private_thread
+
+ A private thread
+
+ .. versionadded:: 1.8
+
.. class:: MessageType
Specifies the type of :class:`Message`. This is used to denote if a message
@@ -1129,9 +1168,14 @@ of :class:`enum.Enum`.
Discovery requirements for 3 weeks in a row.
.. versionadded:: 1.7
+ .. attribute:: thread_created
+
+ The system message denoting that a thread has been created
+
+ .. versionadded:: 2.0
.. attribute:: reply
- The message type denoting that the author is replying to a message.
+ The system message denoting that the author is replying to a message.
.. versionadded:: 2.0
.. attribute:: application_command
@@ -1144,6 +1188,12 @@ of :class:`enum.Enum`.
The system message sent as a reminder to invite people to the guild.
.. versionadded:: 2.0
+ .. attribute:: thread_starter_message
+
+ The system message denoting that this message is the one that started a thread's
+ conversation topic.
+
+ .. versionadded:: 2.0
.. class:: UserFlags
@@ -3197,6 +3247,30 @@ TextChannel
.. automethod:: typing
:async-with:
+Thread
+~~~~~~~~
+
+.. attributetable:: Thread
+
+.. autoclass:: Thread()
+ :members:
+ :inherited-members:
+ :exclude-members: history, typing
+
+ .. automethod:: history
+ :async-for:
+
+ .. automethod:: typing
+ :async-with:
+
+ThreadMember
+~~~~~~~~~~~~~
+
+.. attributetable:: ThreadMember
+
+.. autoclass:: ThreadMember()
+ :members:
+
StoreChannel
~~~~~~~~~~~~~