mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-09-06 09:56:09 +00:00
Add support for newest ForumChannel changes
This adds the following: - Forum tag support - Default reaction support - Default slowmode for newly created threads
This commit is contained in:
@ -55,6 +55,8 @@ from .asset import Asset
|
||||
from .errors import ClientException
|
||||
from .stage_instance import StageInstance
|
||||
from .threads import Thread
|
||||
from .partial_emoji import _EmojiTag, PartialEmoji
|
||||
from .flags import ChannelFlags
|
||||
from .http import handle_message_parameters
|
||||
|
||||
__all__ = (
|
||||
@ -63,6 +65,7 @@ __all__ = (
|
||||
'StageChannel',
|
||||
'DMChannel',
|
||||
'CategoryChannel',
|
||||
'ForumTag',
|
||||
'ForumChannel',
|
||||
'GroupChannel',
|
||||
'PartialMessageable',
|
||||
@ -77,7 +80,7 @@ if TYPE_CHECKING:
|
||||
from .member import Member, VoiceState
|
||||
from .abc import Snowflake, SnowflakeTime
|
||||
from .embeds import Embed
|
||||
from .message import Message, PartialMessage
|
||||
from .message import Message, PartialMessage, EmojiInputType
|
||||
from .mentions import AllowedMentions
|
||||
from .webhook import Webhook
|
||||
from .state import ConnectionState
|
||||
@ -95,6 +98,7 @@ if TYPE_CHECKING:
|
||||
CategoryChannel as CategoryChannelPayload,
|
||||
GroupDMChannel as GroupChannelPayload,
|
||||
ForumChannel as ForumChannelPayload,
|
||||
ForumTag as ForumTagPayload,
|
||||
)
|
||||
from .types.snowflake import SnowflakeList
|
||||
|
||||
@ -156,6 +160,10 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
The default auto archive duration in minutes for threads created in this channel.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
default_thread_slowmode_delay: :class:`int`
|
||||
The default slowmode delay in seconds for threads created in this channel.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
@ -172,6 +180,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
'_type',
|
||||
'last_message_id',
|
||||
'default_auto_archive_duration',
|
||||
'default_thread_slowmode_delay',
|
||||
)
|
||||
|
||||
def __init__(self, *, state: ConnectionState, guild: Guild, data: Union[TextChannelPayload, NewsChannelPayload]):
|
||||
@ -202,6 +211,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
# Does this need coercion into `int`? No idea yet.
|
||||
self.slowmode_delay: int = data.get('rate_limit_per_user', 0)
|
||||
self.default_auto_archive_duration: ThreadArchiveDuration = data.get('default_auto_archive_duration', 1440)
|
||||
self.default_thread_slowmode_delay: int = data.get('default_thread_rate_limit_per_user', 0)
|
||||
self._type: Literal[0, 5] = data.get('type', self._type)
|
||||
self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id')
|
||||
self._fill_overwrites(data)
|
||||
@ -296,6 +306,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
category: Optional[CategoryChannel] = ...,
|
||||
slowmode_delay: int = ...,
|
||||
default_auto_archive_duration: ThreadArchiveDuration = ...,
|
||||
default_thread_slowmode_delay: int = ...,
|
||||
type: ChannelType = ...,
|
||||
overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ...,
|
||||
) -> TextChannel:
|
||||
@ -354,6 +365,12 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
The new default auto archive duration in minutes for threads created in this channel.
|
||||
Must be one of ``60``, ``1440``, ``4320``, or ``10080``.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
default_thread_slowmode_delay: :class:`int`
|
||||
The new default slowmode delay in seconds for threads created in this channel.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
@ -1967,6 +1984,89 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
return await self.guild.create_forum(name, category=self, **options)
|
||||
|
||||
|
||||
class ForumTag(Hashable):
|
||||
"""Represents a forum tag that can be applied to a thread within a :class:`ForumChannel`.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two forum tags are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two forum tags are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the forum tag's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the forum tag's name.
|
||||
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The ID of the tag. If this was manually created then the ID will be ``0``.
|
||||
name: :class:`str`
|
||||
The name of the tag. Can only be up to 20 characters.
|
||||
moderated: :class:`bool`
|
||||
Whether this tag can only be added or removed by a moderator with
|
||||
the :attr:`~Permissions.manage_threads` permission.
|
||||
emoji: :class:`PartialEmoji`
|
||||
The emoji that is used to represent this tag.
|
||||
Note that if the emoji is a custom emoji, it will *not* have name information.
|
||||
"""
|
||||
|
||||
__slots__ = ('name', 'id', 'moderated', 'emoji')
|
||||
|
||||
def __init__(self, *, name: str, emoji: EmojiInputType, moderated: bool = False) -> None:
|
||||
self.name: str = name
|
||||
self.id: int = 0
|
||||
self.moderated: bool = moderated
|
||||
self.emoji: PartialEmoji
|
||||
if isinstance(emoji, _EmojiTag):
|
||||
self.emoji = emoji._to_partial()
|
||||
elif isinstance(emoji, str):
|
||||
self.emoji = PartialEmoji.from_str(emoji)
|
||||
else:
|
||||
raise TypeError(f'emoji must be a Emoji, PartialEmoji, or str not {emoji.__class__!r}')
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, *, state: ConnectionState, data: ForumTagPayload) -> Self:
|
||||
self = cls.__new__(cls)
|
||||
self.name = data['name']
|
||||
self.id = int(data['id'])
|
||||
self.moderated = data.get('moderated', False)
|
||||
|
||||
emoji_name = data['emoji_name'] or ''
|
||||
emoji_id = utils._get_as_snowflake(data, 'emoji_id') or None # Coerce 0 -> None
|
||||
self.emoji = PartialEmoji.with_state(state=state, name=emoji_name, id=emoji_id)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
'name': self.name,
|
||||
'moderated': self.moderated,
|
||||
}
|
||||
payload.update(self.emoji._to_forum_tag_payload())
|
||||
|
||||
if self.id:
|
||||
payload['id'] = self.id
|
||||
|
||||
return payload
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<ForumTag id={self.id} name={self.name!r} emoji={self.emoji!r} moderated={self.moderated}>'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
"""Represents a Discord guild forum channel.
|
||||
|
||||
@ -2001,7 +2101,8 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
category_id: Optional[:class:`int`]
|
||||
The category channel ID this forum belongs to, if applicable.
|
||||
topic: Optional[:class:`str`]
|
||||
The forum's topic. ``None`` if it doesn't exist.
|
||||
The forum's topic. ``None`` if it doesn't exist. Called "Guidelines" in the UI.
|
||||
Can be up to 4096 characters long.
|
||||
position: :class:`int`
|
||||
The position in the channel list. This is a number that starts at 0. e.g. the
|
||||
top channel is position 0.
|
||||
@ -2018,6 +2119,15 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
If the forum is marked as "not safe for work" or "age restricted".
|
||||
default_auto_archive_duration: :class:`int`
|
||||
The default auto archive duration in minutes for threads created in this forum.
|
||||
default_thread_slowmode_delay: :class:`int`
|
||||
The default slowmode delay in seconds for threads created in this forum.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
default_reaction_emoji: Optional[:class:`PartialEmoji`]
|
||||
The default reaction emoji for threads created in this forum to show in the
|
||||
add reaction button.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
@ -2034,6 +2144,10 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
'_overwrites',
|
||||
'last_message_id',
|
||||
'default_auto_archive_duration',
|
||||
'default_thread_slowmode_delay',
|
||||
'default_reaction_emoji',
|
||||
'_available_tags',
|
||||
'_flags',
|
||||
)
|
||||
|
||||
def __init__(self, *, state: ConnectionState, guild: Guild, data: ForumChannelPayload):
|
||||
@ -2062,6 +2176,21 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
self.slowmode_delay: int = data.get('rate_limit_per_user', 0)
|
||||
self.default_auto_archive_duration: ThreadArchiveDuration = data.get('default_auto_archive_duration', 1440)
|
||||
self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id')
|
||||
# This takes advantage of the fact that dicts are ordered since Python 3.7
|
||||
tags = [ForumTag.from_data(state=self._state, data=tag) for tag in data.get('available_tags', [])]
|
||||
self.default_thread_slowmode_delay: int = data.get('default_thread_slowmode_delay', 0)
|
||||
self._available_tags: Dict[int, ForumTag] = {tag.id: tag for tag in tags}
|
||||
|
||||
self.default_reaction_emoji: Optional[PartialEmoji] = None
|
||||
default_reaction_emoji = data.get('default_reaction_emoji')
|
||||
if default_reaction_emoji:
|
||||
self.default_reaction_emoji = PartialEmoji.with_state(
|
||||
state=self._state,
|
||||
id=utils._get_as_snowflake(default_reaction_emoji, 'emoji_id') or None, # Coerce 0 -> None
|
||||
name=default_reaction_emoji.get('emoji_name') or '',
|
||||
)
|
||||
|
||||
self._flags: int = data.get('flags', 0)
|
||||
self._fill_overwrites(data)
|
||||
|
||||
@property
|
||||
@ -2091,6 +2220,39 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
"""List[:class:`Thread`]: Returns all the threads that you can see."""
|
||||
return [thread for thread in self.guild._threads.values() if thread.parent_id == self.id]
|
||||
|
||||
@property
|
||||
def flags(self) -> ChannelFlags:
|
||||
""":class:`ChannelFlags`: The flags associated with this thread.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
"""
|
||||
return ChannelFlags._from_value(self._flags)
|
||||
|
||||
@property
|
||||
def available_tags(self) -> Sequence[ForumTag]:
|
||||
"""Sequence[:class:`ForumTag`]: Returns all the available tags for this forum.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
"""
|
||||
return utils.SequenceProxy(self._available_tags.values())
|
||||
|
||||
def get_tag(self, tag_id: int, /) -> Optional[ForumTag]:
|
||||
"""Returns the tag with the given ID.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tag_id: :class:`int`
|
||||
The ID to search for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`ForumTag`]
|
||||
The tag with the given ID, or ``None`` if not found.
|
||||
"""
|
||||
return self._available_tags.get(tag_id)
|
||||
|
||||
def is_nsfw(self) -> bool:
|
||||
""":class:`bool`: Checks if the forum is NSFW."""
|
||||
return self.nsfw
|
||||
@ -2124,6 +2286,10 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
default_auto_archive_duration: ThreadArchiveDuration = ...,
|
||||
type: ChannelType = ...,
|
||||
overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ...,
|
||||
available_tags: Sequence[ForumTag] = ...,
|
||||
default_thread_slowmode_delay: int = ...,
|
||||
default_reaction_emoji: Optional[EmojiInputType] = ...,
|
||||
require_tag: bool = ...,
|
||||
) -> ForumChannel:
|
||||
...
|
||||
|
||||
@ -2166,6 +2332,22 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
default_auto_archive_duration: :class:`int`
|
||||
The new default auto archive duration in minutes for threads created in this channel.
|
||||
Must be one of ``60``, ``1440``, ``4320``, or ``10080``.
|
||||
available_tags: Sequence[:class:`ForumTag`]
|
||||
The new available tags for this forum.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
default_thread_slowmode_delay: :class:`int`
|
||||
The new default slowmode delay for threads in this channel.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
default_reaction_emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]]
|
||||
The new default reaction emoji for threads in this channel.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
require_tag: :class:`bool`
|
||||
Whether to require a tag for threads in this channel or not.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
Raises
|
||||
------
|
||||
@ -2185,11 +2367,91 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
then ``None`` is returned instead.
|
||||
"""
|
||||
|
||||
try:
|
||||
tags: Sequence[ForumTag] = options.pop('available_tags')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
options['available_tags'] = [tag.to_dict() for tag in tags]
|
||||
|
||||
try:
|
||||
default_reaction_emoji: Optional[EmojiInputType] = options.pop('default_reaction_emoji')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if default_reaction_emoji is None:
|
||||
options['default_reaction_emoji'] = None
|
||||
elif isinstance(default_reaction_emoji, _EmojiTag):
|
||||
options['default_reaction_emoji'] = default_reaction_emoji._to_partial()._to_forum_tag_payload()
|
||||
elif isinstance(default_reaction_emoji, str):
|
||||
options['default_reaction_emoji'] = PartialEmoji.from_str(default_reaction_emoji)._to_forum_tag_payload()
|
||||
|
||||
try:
|
||||
require_tag = options.pop('require_tag')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
flags = self.flags
|
||||
flags.require_tag = require_tag
|
||||
options['flags'] = flags.value
|
||||
|
||||
payload = await self._edit(options, reason=reason)
|
||||
if payload is not None:
|
||||
# the payload will always be the proper channel payload
|
||||
return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore
|
||||
|
||||
async def create_tag(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
emoji: PartialEmoji,
|
||||
moderated: bool = False,
|
||||
reason: Optional[str] = None,
|
||||
) -> ForumTag:
|
||||
"""|coro|
|
||||
|
||||
Creates a new tag in this forum.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: :class:`str`
|
||||
The name of the tag. Can only be up to 20 characters.
|
||||
emoji: Union[:class:`str`, :class:`PartialEmoji`]
|
||||
The emoji to use for the tag.
|
||||
moderated: :class:`bool`
|
||||
Whether the tag can only be applied by moderators.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for creating this tag. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
------
|
||||
Forbidden
|
||||
You do not have permissions to create a tag in this forum.
|
||||
HTTPException
|
||||
Creating the tag failed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`ForumTag`
|
||||
The newly created tag.
|
||||
"""
|
||||
|
||||
prior = list(self._available_tags.values())
|
||||
result = ForumTag(name=name, emoji=emoji, moderated=moderated)
|
||||
prior.append(result)
|
||||
payload = await self._state.http.edit_channel(
|
||||
self.id, reason=reason, available_tags=[tag.to_dict() for tag in prior]
|
||||
)
|
||||
try:
|
||||
result.id = int(payload['available_tags'][-1]['id']) # type: ignore
|
||||
except (KeyError, IndexError, ValueError):
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
async def create_thread(
|
||||
self,
|
||||
*,
|
||||
|
Reference in New Issue
Block a user