mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-04-19 15:36:02 +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:
parent
bdda31307b
commit
ab265dcb7c
@ -418,6 +418,11 @@ class GuildChannel:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
options['default_thread_rate_limit_per_user'] = options.pop('default_thread_slowmode_delay')
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
rtc_region = options.pop('rtc_region')
|
||||
except KeyError:
|
||||
|
@ -36,13 +36,14 @@ from .permissions import PermissionOverwrite, Permissions
|
||||
from .automod import AutoModTrigger, AutoModRuleAction, AutoModPresets, AutoModRule
|
||||
from .role import Role
|
||||
from .emoji import Emoji
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .member import Member
|
||||
from .scheduled_event import ScheduledEvent
|
||||
from .stage_instance import StageInstance
|
||||
from .sticker import GuildSticker
|
||||
from .threads import Thread
|
||||
from .integrations import PartialIntegration
|
||||
from .channel import StageChannel
|
||||
from .channel import ForumChannel, StageChannel, ForumTag
|
||||
|
||||
__all__ = (
|
||||
'AuditLogDiff',
|
||||
@ -63,6 +64,8 @@ if TYPE_CHECKING:
|
||||
)
|
||||
from .types.channel import (
|
||||
PermissionOverwrite as PermissionOverwritePayload,
|
||||
ForumTag as ForumTagPayload,
|
||||
DefaultReaction as DefaultReactionPayload,
|
||||
)
|
||||
from .types.invite import Invite as InvitePayload
|
||||
from .types.role import Role as RolePayload
|
||||
@ -130,6 +133,44 @@ def _transform_roles(entry: AuditLogEntry, data: List[Snowflake]) -> List[Union[
|
||||
return [entry.guild.get_role(int(role_id)) or Object(role_id, type=Role) for role_id in data]
|
||||
|
||||
|
||||
def _transform_applied_forum_tags(entry: AuditLogEntry, data: List[Snowflake]) -> List[Union[ForumTag, Object]]:
|
||||
thread = entry.target
|
||||
if isinstance(thread, Thread) and isinstance(thread.parent, ForumChannel):
|
||||
return [thread.parent.get_tag(tag_id) or Object(id=tag_id, type=ForumTag) for tag_id in map(int, data)]
|
||||
return [Object(id=tag_id, type=ForumTag) for tag_id in data]
|
||||
|
||||
|
||||
def _transform_overloaded_flags(entry: AuditLogEntry, data: int) -> Union[int, flags.ChannelFlags]:
|
||||
# The `flags` key is definitely overloaded. Right now it's for channels and threads but
|
||||
# I am aware of `member.flags` and `user.flags` existing. However, this does not impact audit logs
|
||||
# at the moment but better safe than sorry.
|
||||
channel_audit_log_types = (
|
||||
enums.AuditLogAction.channel_create,
|
||||
enums.AuditLogAction.channel_update,
|
||||
enums.AuditLogAction.channel_delete,
|
||||
enums.AuditLogAction.thread_create,
|
||||
enums.AuditLogAction.thread_update,
|
||||
enums.AuditLogAction.thread_delete,
|
||||
)
|
||||
|
||||
if entry.action in channel_audit_log_types:
|
||||
return flags.ChannelFlags._from_value(data)
|
||||
return data
|
||||
|
||||
|
||||
def _transform_forum_tags(entry: AuditLogEntry, data: List[ForumTagPayload]) -> List[ForumTag]:
|
||||
return [ForumTag.from_data(state=entry._state, data=d) for d in data]
|
||||
|
||||
|
||||
def _transform_default_reaction(entry: AuditLogEntry, data: DefaultReactionPayload) -> Optional[PartialEmoji]:
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
emoji_name = data.get('emoji_name') or ''
|
||||
emoji_id = utils._get_as_snowflake(data, 'emoji_id') or None # Coerce 0 -> None
|
||||
return PartialEmoji.with_state(state=entry._state, name=emoji_name, id=emoji_id)
|
||||
|
||||
|
||||
def _transform_overwrites(
|
||||
entry: AuditLogEntry, data: List[PermissionOverwritePayload]
|
||||
) -> List[Tuple[Object, PermissionOverwrite]]:
|
||||
@ -272,49 +313,54 @@ Transformer = Callable[["AuditLogEntry", Any], Any]
|
||||
class AuditLogChanges:
|
||||
# fmt: off
|
||||
TRANSFORMERS: ClassVar[Dict[str, Tuple[Optional[str], Optional[Transformer]]]] = {
|
||||
'verification_level': (None, _enum_transformer(enums.VerificationLevel)),
|
||||
'explicit_content_filter': (None, _enum_transformer(enums.ContentFilter)),
|
||||
'allow': (None, _flag_transformer(Permissions)),
|
||||
'deny': (None, _flag_transformer(Permissions)),
|
||||
'permissions': (None, _flag_transformer(Permissions)),
|
||||
'id': (None, _transform_snowflake),
|
||||
'color': ('colour', _transform_color),
|
||||
'owner_id': ('owner', _transform_member_id),
|
||||
'inviter_id': ('inviter', _transform_member_id),
|
||||
'channel_id': ('channel', _transform_channel),
|
||||
'afk_channel_id': ('afk_channel', _transform_channel),
|
||||
'system_channel_id': ('system_channel', _transform_channel),
|
||||
'system_channel_flags': (None, _flag_transformer(flags.SystemChannelFlags)),
|
||||
'widget_channel_id': ('widget_channel', _transform_channel),
|
||||
'rules_channel_id': ('rules_channel', _transform_channel),
|
||||
'public_updates_channel_id': ('public_updates_channel', _transform_channel),
|
||||
'permission_overwrites': ('overwrites', _transform_overwrites),
|
||||
'splash_hash': ('splash', _guild_hash_transformer('splashes')),
|
||||
'banner_hash': ('banner', _guild_hash_transformer('banners')),
|
||||
'discovery_splash_hash': ('discovery_splash', _guild_hash_transformer('discovery-splashes')),
|
||||
'icon_hash': ('icon', _transform_icon),
|
||||
'avatar_hash': ('avatar', _transform_avatar),
|
||||
'rate_limit_per_user': ('slowmode_delay', None),
|
||||
'guild_id': ('guild', _transform_guild_id),
|
||||
'tags': ('emoji', None),
|
||||
'default_message_notifications': ('default_notifications', _enum_transformer(enums.NotificationLevel)),
|
||||
'video_quality_mode': (None, _enum_transformer(enums.VideoQualityMode)),
|
||||
'privacy_level': (None, _enum_transformer(enums.PrivacyLevel)),
|
||||
'format_type': (None, _enum_transformer(enums.StickerFormatType)),
|
||||
'type': (None, _transform_type),
|
||||
'communication_disabled_until': ('timed_out_until', _transform_timestamp),
|
||||
'expire_behavior': (None, _enum_transformer(enums.ExpireBehaviour)),
|
||||
'mfa_level': (None, _enum_transformer(enums.MFALevel)),
|
||||
'status': (None, _enum_transformer(enums.EventStatus)),
|
||||
'entity_type': (None, _enum_transformer(enums.EntityType)),
|
||||
'preferred_locale': (None, _enum_transformer(enums.Locale)),
|
||||
'image_hash': ('cover_image', _transform_cover_image),
|
||||
'trigger_type': (None, _enum_transformer(enums.AutoModRuleTriggerType)),
|
||||
'event_type': (None, _enum_transformer(enums.AutoModRuleEventType)),
|
||||
'trigger_metadata': ('trigger', _transform_automod_trigger_metadata),
|
||||
'actions': (None, _transform_automod_actions),
|
||||
'exempt_channels': (None, _transform_channels_or_threads),
|
||||
'exempt_roles': (None, _transform_roles),
|
||||
'verification_level': (None, _enum_transformer(enums.VerificationLevel)),
|
||||
'explicit_content_filter': (None, _enum_transformer(enums.ContentFilter)),
|
||||
'allow': (None, _flag_transformer(Permissions)),
|
||||
'deny': (None, _flag_transformer(Permissions)),
|
||||
'permissions': (None, _flag_transformer(Permissions)),
|
||||
'id': (None, _transform_snowflake),
|
||||
'color': ('colour', _transform_color),
|
||||
'owner_id': ('owner', _transform_member_id),
|
||||
'inviter_id': ('inviter', _transform_member_id),
|
||||
'channel_id': ('channel', _transform_channel),
|
||||
'afk_channel_id': ('afk_channel', _transform_channel),
|
||||
'system_channel_id': ('system_channel', _transform_channel),
|
||||
'system_channel_flags': (None, _flag_transformer(flags.SystemChannelFlags)),
|
||||
'widget_channel_id': ('widget_channel', _transform_channel),
|
||||
'rules_channel_id': ('rules_channel', _transform_channel),
|
||||
'public_updates_channel_id': ('public_updates_channel', _transform_channel),
|
||||
'permission_overwrites': ('overwrites', _transform_overwrites),
|
||||
'splash_hash': ('splash', _guild_hash_transformer('splashes')),
|
||||
'banner_hash': ('banner', _guild_hash_transformer('banners')),
|
||||
'discovery_splash_hash': ('discovery_splash', _guild_hash_transformer('discovery-splashes')),
|
||||
'icon_hash': ('icon', _transform_icon),
|
||||
'avatar_hash': ('avatar', _transform_avatar),
|
||||
'rate_limit_per_user': ('slowmode_delay', None),
|
||||
'default_thread_rate_limit_per_user': ('default_thread_slowmode_delay', None),
|
||||
'guild_id': ('guild', _transform_guild_id),
|
||||
'tags': ('emoji', None),
|
||||
'default_message_notifications': ('default_notifications', _enum_transformer(enums.NotificationLevel)),
|
||||
'video_quality_mode': (None, _enum_transformer(enums.VideoQualityMode)),
|
||||
'privacy_level': (None, _enum_transformer(enums.PrivacyLevel)),
|
||||
'format_type': (None, _enum_transformer(enums.StickerFormatType)),
|
||||
'type': (None, _transform_type),
|
||||
'communication_disabled_until': ('timed_out_until', _transform_timestamp),
|
||||
'expire_behavior': (None, _enum_transformer(enums.ExpireBehaviour)),
|
||||
'mfa_level': (None, _enum_transformer(enums.MFALevel)),
|
||||
'status': (None, _enum_transformer(enums.EventStatus)),
|
||||
'entity_type': (None, _enum_transformer(enums.EntityType)),
|
||||
'preferred_locale': (None, _enum_transformer(enums.Locale)),
|
||||
'image_hash': ('cover_image', _transform_cover_image),
|
||||
'trigger_type': (None, _enum_transformer(enums.AutoModRuleTriggerType)),
|
||||
'event_type': (None, _enum_transformer(enums.AutoModRuleEventType)),
|
||||
'trigger_metadata': ('trigger', _transform_automod_trigger_metadata),
|
||||
'actions': (None, _transform_automod_actions),
|
||||
'exempt_channels': (None, _transform_channels_or_threads),
|
||||
'exempt_roles': (None, _transform_roles),
|
||||
'applied_tags': (None, _transform_applied_forum_tags),
|
||||
'available_tags': (None, _transform_forum_tags),
|
||||
'flags': (None, _transform_overloaded_flags),
|
||||
'default_reaction_emoji': (None, _transform_default_reaction),
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
|
@ -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,
|
||||
*,
|
||||
|
@ -1490,6 +1490,15 @@ class ChannelFlags(BaseFlags):
|
||||
""":class:`bool`: Returns ``True`` if the thread is pinned to the forum channel."""
|
||||
return 1 << 1
|
||||
|
||||
@flag_value
|
||||
def require_tag(self):
|
||||
""":class:`bool`: Returns ``True`` if a tag is required to be specified when creating a thread
|
||||
in a :class:`ForumChannel`.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
"""
|
||||
return 1 << 4
|
||||
|
||||
|
||||
class ArrayFlags(BaseFlags):
|
||||
@classmethod
|
||||
|
@ -1143,7 +1143,12 @@ class HTTPClient:
|
||||
'invitable',
|
||||
'default_auto_archive_duration',
|
||||
'flags',
|
||||
'default_thread_rate_limit_per_user',
|
||||
'default_reaction_emoji',
|
||||
'available_tags',
|
||||
'applied_tags',
|
||||
)
|
||||
|
||||
payload = {k: v for k, v in options.items() if k in valid_keys}
|
||||
return self.request(r, reason=reason, json=payload)
|
||||
|
||||
|
@ -162,6 +162,11 @@ class PartialEmoji(_EmojiTag, AssetMixin):
|
||||
def _to_partial(self) -> PartialEmoji:
|
||||
return self
|
||||
|
||||
def _to_forum_tag_payload(self) -> Dict[str, Any]:
|
||||
if self.id is not None:
|
||||
return {'emoji_id': self.id, 'emoji_name': None}
|
||||
return {'emoji_id': None, 'emoji_name': self.name}
|
||||
|
||||
@classmethod
|
||||
def with_state(
|
||||
cls,
|
||||
@ -176,11 +181,13 @@ class PartialEmoji(_EmojiTag, AssetMixin):
|
||||
return self
|
||||
|
||||
def __str__(self) -> str:
|
||||
# Coerce empty names to _ so it renders in the client regardless of having no name
|
||||
name = self.name or '_'
|
||||
if self.id is None:
|
||||
return self.name
|
||||
return name
|
||||
if self.animated:
|
||||
return f'<a:{self.name}:{self.id}>'
|
||||
return f'<:{self.name}:{self.id}>'
|
||||
return f'<a:{name}:{self.id}>'
|
||||
return f'<:{name}:{self.id}>'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} animated={self.animated} name={self.name!r} id={self.id}>'
|
||||
|
@ -24,15 +24,16 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Dict, Iterable, List, Literal, Optional, Union, TYPE_CHECKING
|
||||
from typing import Callable, Dict, Iterable, List, Literal, Optional, Sequence, Union, TYPE_CHECKING
|
||||
from datetime import datetime
|
||||
import array
|
||||
|
||||
from .mixins import Hashable
|
||||
from .abc import Messageable, _purge_helper
|
||||
from .enums import ChannelType, try_enum
|
||||
from .errors import ClientException
|
||||
from .flags import ChannelFlags
|
||||
from .utils import MISSING, parse_time, _get_as_snowflake
|
||||
from .utils import MISSING, parse_time, _get_as_snowflake, _unique
|
||||
|
||||
__all__ = (
|
||||
'Thread',
|
||||
@ -50,7 +51,7 @@ if TYPE_CHECKING:
|
||||
)
|
||||
from .types.snowflake import SnowflakeList
|
||||
from .guild import Guild
|
||||
from .channel import TextChannel, CategoryChannel, ForumChannel
|
||||
from .channel import TextChannel, CategoryChannel, ForumChannel, ForumTag
|
||||
from .member import Member
|
||||
from .message import Message, PartialMessage
|
||||
from .abc import Snowflake, SnowflakeTime
|
||||
@ -149,6 +150,7 @@ class Thread(Messageable, Hashable):
|
||||
'archive_timestamp',
|
||||
'_created_at',
|
||||
'_flags',
|
||||
'_applied_tags',
|
||||
)
|
||||
|
||||
def __init__(self, *, guild: Guild, state: ConnectionState, data: ThreadPayload) -> None:
|
||||
@ -180,6 +182,8 @@ class Thread(Messageable, Hashable):
|
||||
self.message_count: int = data['message_count']
|
||||
self.member_count: int = data['member_count']
|
||||
self._flags: int = data.get('flags', 0)
|
||||
# SnowflakeList is sorted, but this would not be proper for applied tags, where order actually matters.
|
||||
self._applied_tags: array.array[int] = array.array('Q', map(int, data.get('applied_tags', [])))
|
||||
self._unroll_metadata(data['thread_metadata'])
|
||||
|
||||
self.me: Optional[ThreadMember]
|
||||
@ -255,6 +259,24 @@ class Thread(Messageable, Hashable):
|
||||
"""
|
||||
return list(self._members.values())
|
||||
|
||||
@property
|
||||
def applied_tags(self) -> List[ForumTag]:
|
||||
"""List[:class:`ForumTag`]: A list of tags applied to this thread.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
"""
|
||||
tags = []
|
||||
if self.parent is None or self.parent.type != ChannelType.forum:
|
||||
return tags
|
||||
|
||||
parent = self.parent
|
||||
for tag_id in self._applied_tags:
|
||||
tag = parent.get_tag(tag_id)
|
||||
if tag is not None:
|
||||
tags.append(tag)
|
||||
|
||||
return tags
|
||||
|
||||
@property
|
||||
def starter_message(self) -> Optional[Message]:
|
||||
"""Returns the thread starter message from the cache.
|
||||
@ -542,6 +564,7 @@ class Thread(Messageable, Hashable):
|
||||
pinned: bool = MISSING,
|
||||
slowmode_delay: int = MISSING,
|
||||
auto_archive_duration: ThreadArchiveDuration = MISSING,
|
||||
applied_tags: Sequence[ForumTag] = MISSING,
|
||||
reason: Optional[str] = None,
|
||||
) -> Thread:
|
||||
"""|coro|
|
||||
@ -574,6 +597,10 @@ class Thread(Messageable, Hashable):
|
||||
slowmode_delay: :class:`int`
|
||||
Specifies the slowmode rate limit for user in this thread, in seconds.
|
||||
A value of ``0`` disables slowmode. The maximum value possible is ``21600``.
|
||||
applied_tags: Sequence[:class:`ForumTag`]
|
||||
The new tags to apply to the thread. There can only be up to 5 tags applied to a thread.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for editing this thread. Shows up on the audit log.
|
||||
|
||||
@ -606,11 +633,87 @@ class Thread(Messageable, Hashable):
|
||||
flags = self.flags
|
||||
flags.pinned = pinned
|
||||
payload['flags'] = flags.value
|
||||
if applied_tags is not MISSING:
|
||||
payload['applied_tags'] = [str(tag.id) for tag in applied_tags]
|
||||
|
||||
data = await self._state.http.edit_channel(self.id, **payload, reason=reason)
|
||||
# The data payload will always be a Thread payload
|
||||
return Thread(data=data, state=self._state, guild=self.guild) # type: ignore
|
||||
|
||||
async def add_tags(self, *tags: Snowflake, reason: Optional[str] = None) -> None:
|
||||
r"""|coro|
|
||||
|
||||
Adds the given forum tags to a thread.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_threads` permission to
|
||||
use this or the thread must be owned by you.
|
||||
|
||||
Tags that have :attr:`ForumTag.moderated` set to ``True`` require the
|
||||
:attr:`~Permissions.manage_threads` permissions to be added.
|
||||
|
||||
The maximum number of tags that can be added to a thread is 5.
|
||||
|
||||
The parent channel must be a :class:`ForumChannel`.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*tags: :class:`abc.Snowflake`
|
||||
An argument list of :class:`abc.Snowflake` representing a :class:`ForumTag`
|
||||
to add to the thread.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for adding these tags.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to add these tags.
|
||||
HTTPException
|
||||
Adding tags failed.
|
||||
"""
|
||||
|
||||
applied_tags = [str(tag) for tag in self._applied_tags]
|
||||
applied_tags.extend(str(tag.id) for tag in tags)
|
||||
|
||||
await self._state.http.edit_channel(self.id, applied_tags=_unique(applied_tags), reason=reason)
|
||||
|
||||
async def remove_tags(self, *tags: Snowflake, reason: Optional[str] = None) -> None:
|
||||
r"""|coro|
|
||||
|
||||
Remove the given forum tags to a thread.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_threads` permission to
|
||||
use this or the thread must be owned by you.
|
||||
|
||||
The parent channel must be a :class:`ForumChannel`.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*tags: :class:`abc.Snowflake`
|
||||
An argument list of :class:`abc.Snowflake` representing a :class:`ForumTag`
|
||||
to remove to the thread.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for removing these tags.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to remove these tags.
|
||||
HTTPException
|
||||
Removing tags failed.
|
||||
"""
|
||||
|
||||
# Once again, taking advantage of the fact that dicts are ordered since 3.7
|
||||
applied_tags: Dict[str, Literal[None]] = {str(tag): None for tag in self._applied_tags}
|
||||
|
||||
for tag in tags:
|
||||
applied_tags.pop(str(tag.id), None)
|
||||
|
||||
await self._state.http.edit_channel(self.id, applied_tags=list(applied_tags.keys()), reason=reason)
|
||||
|
||||
async def join(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
|
@ -34,7 +34,7 @@ from .user import User
|
||||
from .scheduled_event import EntityType, EventStatus, GuildScheduledEvent
|
||||
from .snowflake import Snowflake
|
||||
from .role import Role
|
||||
from .channel import ChannelType, PrivacyLevel, VideoQualityMode, PermissionOverwrite
|
||||
from .channel import ChannelType, DefaultReaction, PrivacyLevel, VideoQualityMode, PermissionOverwrite, ForumTag
|
||||
from .threads import Thread
|
||||
from .command import ApplicationCommand, ApplicationCommandPermissions
|
||||
|
||||
@ -87,6 +87,12 @@ AuditLogEvent = Literal[
|
||||
111,
|
||||
112,
|
||||
121,
|
||||
140,
|
||||
141,
|
||||
142,
|
||||
143,
|
||||
144,
|
||||
145,
|
||||
]
|
||||
|
||||
|
||||
@ -166,7 +172,9 @@ class _AuditLogChange_Int(TypedDict):
|
||||
'user_limit',
|
||||
'auto_archive_duration',
|
||||
'default_auto_archive_duration',
|
||||
'default_thread_rate_limit_per_user',
|
||||
'communication_disabled_until',
|
||||
'flags',
|
||||
]
|
||||
new_value: int
|
||||
old_value: int
|
||||
@ -250,6 +258,24 @@ class _AuditLogChange_AppCommandPermissions(TypedDict):
|
||||
old_value: ApplicationCommandPermissions
|
||||
|
||||
|
||||
class _AuditLogChange_AppliedTags(TypedDict):
|
||||
key: Literal['applied_tags']
|
||||
new_value: List[Snowflake]
|
||||
old_value: List[Snowflake]
|
||||
|
||||
|
||||
class _AuditLogChange_AvailableTags(TypedDict):
|
||||
key: Literal['available_tags']
|
||||
new_value: List[ForumTag]
|
||||
old_value: List[ForumTag]
|
||||
|
||||
|
||||
class _AuditLogChange_DefaultReactionEmoji(TypedDict):
|
||||
key: Literal['default_reaction_emoji']
|
||||
new_value: Optional[DefaultReaction]
|
||||
old_value: Optional[DefaultReaction]
|
||||
|
||||
|
||||
AuditLogChange = Union[
|
||||
_AuditLogChange_Str,
|
||||
_AuditLogChange_AssetHash,
|
||||
@ -269,6 +295,9 @@ AuditLogChange = Union[
|
||||
_AuditLogChange_Status,
|
||||
_AuditLogChange_EntityType,
|
||||
_AuditLogChange_AppCommandPermissions,
|
||||
_AuditLogChange_AppliedTags,
|
||||
_AuditLogChange_AvailableTags,
|
||||
_AuditLogChange_DefaultReactionEmoji,
|
||||
]
|
||||
|
||||
|
||||
|
@ -66,6 +66,7 @@ class _BaseTextChannel(_BaseGuildChannel, total=False):
|
||||
last_message_id: Optional[Snowflake]
|
||||
last_pin_timestamp: str
|
||||
rate_limit_per_user: int
|
||||
default_thread_rate_limit_per_user: int
|
||||
default_auto_archive_duration: ThreadArchiveDuration
|
||||
|
||||
|
||||
@ -117,10 +118,27 @@ class ThreadChannel(_BaseChannel):
|
||||
last_message_id: NotRequired[Optional[Snowflake]]
|
||||
last_pin_timestamp: NotRequired[str]
|
||||
flags: NotRequired[int]
|
||||
applied_tags: NotRequired[List[Snowflake]]
|
||||
|
||||
|
||||
class DefaultReaction(TypedDict):
|
||||
emoji_id: Optional[Snowflake]
|
||||
emoji_name: Optional[str]
|
||||
|
||||
|
||||
class ForumTag(TypedDict):
|
||||
id: Snowflake
|
||||
name: str
|
||||
moderated: bool
|
||||
emoji_id: Optional[Snowflake]
|
||||
emoji_name: Optional[str]
|
||||
|
||||
|
||||
class ForumChannel(_BaseTextChannel):
|
||||
type: Literal[15]
|
||||
available_tags: List[ForumTag]
|
||||
default_reaction_emoji: Optional[DefaultReaction]
|
||||
flags: NotRequired[int]
|
||||
|
||||
|
||||
GuildChannel = Union[TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, ThreadChannel, ForumChannel]
|
||||
|
32
docs/api.rst
32
docs/api.rst
@ -4397,6 +4397,14 @@ GuildSticker
|
||||
.. autoclass:: GuildSticker()
|
||||
:members:
|
||||
|
||||
ShardInfo
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: ShardInfo
|
||||
|
||||
.. autoclass:: ShardInfo()
|
||||
:members:
|
||||
|
||||
RawMessageDeleteEvent
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -4710,20 +4718,12 @@ PermissionOverwrite
|
||||
.. autoclass:: PermissionOverwrite
|
||||
:members:
|
||||
|
||||
ShardInfo
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: ShardInfo
|
||||
|
||||
.. autoclass:: ShardInfo()
|
||||
:members:
|
||||
|
||||
SystemChannelFlags
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: SystemChannelFlags
|
||||
|
||||
.. autoclass:: SystemChannelFlags()
|
||||
.. autoclass:: SystemChannelFlags
|
||||
:members:
|
||||
|
||||
MessageFlags
|
||||
@ -4731,7 +4731,7 @@ MessageFlags
|
||||
|
||||
.. attributetable:: MessageFlags
|
||||
|
||||
.. autoclass:: MessageFlags()
|
||||
.. autoclass:: MessageFlags
|
||||
:members:
|
||||
|
||||
PublicUserFlags
|
||||
@ -4739,9 +4739,19 @@ PublicUserFlags
|
||||
|
||||
.. attributetable:: PublicUserFlags
|
||||
|
||||
.. autoclass:: PublicUserFlags()
|
||||
.. autoclass:: PublicUserFlags
|
||||
:members:
|
||||
|
||||
|
||||
ForumTag
|
||||
~~~~~~~~~
|
||||
|
||||
.. attributetable:: ForumTag
|
||||
|
||||
.. autoclass:: ForumTag
|
||||
:members:
|
||||
|
||||
|
||||
Exceptions
|
||||
------------
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user