diff --git a/discord/__init__.py b/discord/__init__.py index e3148e513..765719b68 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __title__ = 'discord' __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.4.0a' +__version__ = '2.5.0a' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -80,7 +80,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=4, micro=0, releaselevel='alpha', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=0, releaselevel='alpha', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/discord/abc.py b/discord/abc.py index 656a38659..fec57b52a 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -49,7 +49,7 @@ from typing import ( from .object import OLDEST_OBJECT, Object from .context_managers import Typing from .enums import ChannelType, InviteTarget -from .errors import ClientException +from .errors import ClientException, NotFound from .mentions import AllowedMentions from .permissions import PermissionOverwrite, Permissions from .role import Role @@ -122,7 +122,14 @@ _undefined: Any = _Undefined() async def _single_delete_strategy(messages: Iterable[Message], *, reason: Optional[str] = None): for m in messages: - await m.delete() + try: + await m.delete() + except NotFound as exc: + if exc.code == 10008: + continue # bulk deletion ignores not found messages, single deletion does not. + # several other race conditions with deletion should fail without continuing, + # such as the channel being deleted and not found. + raise async def _purge_helper( @@ -699,6 +706,7 @@ class GuildChannel: - Member overrides - Implicit permissions - Member timeout + - User installed app If a :class:`~discord.Role` is passed, then it checks the permissions someone with that role would have, which is essentially: @@ -714,6 +722,12 @@ class GuildChannel: .. versionchanged:: 2.0 ``obj`` parameter is now positional-only. + .. versionchanged:: 2.4 + User installed apps are now taken into account. + The permissions returned for a user installed app mirrors the + permissions Discord returns in :attr:`~discord.Interaction.app_permissions`, + though it is recommended to use that attribute instead. + Parameters ---------- obj: Union[:class:`~discord.Member`, :class:`~discord.Role`] @@ -745,6 +759,13 @@ class GuildChannel: return Permissions.all() default = self.guild.default_role + if default is None: + + if self._state.self_id == obj.id: + return Permissions._user_installed_permissions(in_guild=True) + else: + return Permissions.none() + base = Permissions(default.permissions.value) # Handle the role case first diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index e8c9c3c6f..cd6eafaf3 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -2549,6 +2549,8 @@ def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]] Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + .. versionadded:: 2.4 + Examples --------- @@ -2640,6 +2642,8 @@ def allowed_contexts(guilds: bool = MISSING, dms: bool = MISSING, private_channe Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + .. versionadded:: 2.4 + Examples --------- @@ -2691,6 +2695,8 @@ def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + .. versionadded:: 2.4 + Examples --------- @@ -2739,6 +2745,8 @@ def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + .. versionadded:: 2.4 + Examples --------- @@ -2781,6 +2789,8 @@ def allowed_installs( Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + .. versionadded:: 2.4 + Examples --------- diff --git a/discord/guild.py b/discord/guild.py index 3edc88f33..f6c6b7d1b 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4388,7 +4388,7 @@ class Guild(Hashable): actions=[a.to_dict() for a in actions], enabled=enabled, exempt_roles=[str(r.id) for r in exempt_roles] if exempt_roles else None, - exempt_channel=[str(c.id) for c in exempt_channels] if exempt_channels else None, + exempt_channels=[str(c.id) for c in exempt_channels] if exempt_channels else None, reason=reason, ) diff --git a/discord/permissions.py b/discord/permissions.py index 39b0b1a5e..17c7b38c9 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -208,6 +208,22 @@ class Permissions(BaseFlags): base.send_messages_in_threads = False return base + @classmethod + def _user_installed_permissions(cls, *, in_guild: bool) -> Self: + base = cls.none() + base.send_messages = True + base.attach_files = True + base.embed_links = True + base.external_emojis = True + base.send_voice_messages = True + if in_guild: + # Logically this is False but if not set to True, + # permissions just become 0. + base.read_messages = True + base.send_tts_messages = True + base.send_messages_in_threads = True + return base + @classmethod def all_channel(cls) -> Self: """A :class:`Permissions` with all channel-specific permissions set to @@ -241,10 +257,10 @@ class Permissions(BaseFlags): Added :attr:`use_soundboard`, :attr:`create_expressions` permissions. .. versionchanged:: 2.4 - Added :attr:`send_polls`, :attr:`send_voice_messages`, attr:`use_external_sounds`, and - :attr:`use_embedded_activities` permissions. + Added :attr:`send_polls`, :attr:`send_voice_messages`, attr:`use_external_sounds`, + :attr:`use_embedded_activities`, and :attr:`use_external_apps` permissions. """ - return cls(0b0000_0000_0000_0010_0110_0100_1111_1101_1011_0011_1111_0111_1111_1111_0101_0001) + return cls(0b0000_0000_0000_0110_0110_0100_1111_1101_1011_0011_1111_0111_1111_1111_0101_0001) @classmethod def general(cls) -> Self: @@ -291,9 +307,9 @@ class Permissions(BaseFlags): Added :attr:`send_voice_messages` permission. .. versionchanged:: 2.4 - Added :attr:`send_polls` permission. + Added :attr:`send_polls` and :attr:`use_external_apps` permissions. """ - return cls(0b0000_0000_0000_0010_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000) + return cls(0b0000_0000_0000_0110_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000) @classmethod def voice(cls) -> Self: @@ -760,6 +776,14 @@ class Permissions(BaseFlags): """ return 1 << 49 + @flag_value + def use_external_apps(self) -> int: + """:class:`bool`: Returns ``True`` if a user can use external apps. + + .. versionadded:: 2.4 + """ + return 1 << 50 + def _augment_from_permissions(cls): cls.VALID_NAMES = set(Permissions.VALID_FLAGS) @@ -882,6 +906,7 @@ class PermissionOverwrite: create_events: Optional[bool] send_polls: Optional[bool] create_polls: Optional[bool] + use_external_apps: Optional[bool] def __init__(self, **kwargs: Optional[bool]): self._values: Dict[str, Optional[bool]] = {} diff --git a/discord/state.py b/discord/state.py index 032dc2645..db0395266 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1648,13 +1648,8 @@ class ConnectionState(Generic[ClientT]): if message and user: poll = self._update_poll_counts(message, raw.answer_id, True, raw.user_id == self.self_id) - if not poll: - _log.warning( - 'POLL_VOTE_ADD referencing message with ID: %s does not have a poll. Discarding.', raw.message_id - ) - return - - self.dispatch('poll_vote_add', user, poll.get_answer(raw.answer_id)) + if poll: + self.dispatch('poll_vote_add', user, poll.get_answer(raw.answer_id)) def parse_message_poll_vote_remove(self, data: gw.PollVoteActionEvent) -> None: raw = RawPollVoteActionEvent(data) @@ -1671,13 +1666,8 @@ class ConnectionState(Generic[ClientT]): if message and user: poll = self._update_poll_counts(message, raw.answer_id, False, raw.user_id == self.self_id) - if not poll: - _log.warning( - 'POLL_VOTE_REMOVE referencing message with ID: %s does not have a poll. Discarding.', raw.message_id - ) - return - - self.dispatch('poll_vote_remove', user, poll.get_answer(raw.answer_id)) + if poll: + self.dispatch('poll_vote_remove', user, poll.get_answer(raw.answer_id)) def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]: if isinstance(channel, (TextChannel, Thread, VoiceChannel)): diff --git a/discord/ui/button.py b/discord/ui/button.py index 4d306fd10..43bd3a8b0 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -78,7 +78,8 @@ class Button(Item[V]): For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). sku_id: Optional[:class:`int`] - The SKU ID this button sends you to. Can't be combined with ``url``. + The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji`` + nor ``custom_id``. .. versionadded:: 2.4 """ @@ -106,11 +107,15 @@ class Button(Item[V]): sku_id: Optional[int] = None, ): super().__init__() - if custom_id is not None and url is not None: - raise TypeError('cannot mix both url and custom_id with Button') + if custom_id is not None and (url is not None or sku_id is not None): + raise TypeError('cannot mix both url or sku_id and custom_id with Button') + if url is not None and sku_id is not None: + raise TypeError('cannot mix both url and sku_id') + + requires_custom_id = url is None and sku_id is None self._provided_custom_id = custom_id is not None - if url is None and custom_id is None: + if requires_custom_id and custom_id is None: custom_id = os.urandom(16).hex() if custom_id is not None and not isinstance(custom_id, str): @@ -222,7 +227,8 @@ class Button(Item[V]): @sku_id.setter def sku_id(self, value: Optional[int]) -> None: - self.style = ButtonStyle.premium + if value is not None: + self.style = ButtonStyle.premium self._underlying.sku_id = value @classmethod @@ -265,7 +271,6 @@ def button( style: ButtonStyle = ButtonStyle.secondary, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, row: Optional[int] = None, - sku_id: Optional[int] = None, ) -> Callable[[ItemCallbackType[V, Button[V]]], Button[V]]: """A decorator that attaches a button to a component. @@ -275,11 +280,11 @@ def button( .. note:: - Buttons with a URL cannot be created with this function. + Buttons with a URL or an SKU cannot be created with this function. Consider creating a :class:`Button` manually instead. - This is because buttons with a URL do not have a callback + This is because these buttons cannot have a callback associated with them since Discord does not do any processing - with it. + with them. Parameters ------------ @@ -303,10 +308,6 @@ def button( like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). - sku_id: Optional[:class:`int`] - The SKU ID this button sends you to. Can't be combined with ``url``. - - .. versionadded:: 2.4 """ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: @@ -322,7 +323,7 @@ def button( 'label': label, 'emoji': emoji, 'row': row, - 'sku_id': sku_id, + 'sku_id': None, } return func diff --git a/docs/intents.rst b/docs/intents.rst index e805c5ff7..ca85ab8dd 100644 --- a/docs/intents.rst +++ b/docs/intents.rst @@ -114,6 +114,7 @@ Message Content - Whether you use :attr:`Message.attachments` to check message attachments. - Whether you use :attr:`Message.embeds` to check message embeds. - Whether you use :attr:`Message.components` to check message components. +- Whether you use :attr:`Message.poll` to check the message polls. - Whether you use the commands extension with a non-mentioning prefix. .. _intents_member_cache: diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index d467d2b9f..211cd790f 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -336,7 +336,7 @@ Enumerations Represents a link button. .. attribute:: premium - Represents a gradient button denoting that buying a SKU is + Represents a button denoting that buying a SKU is required to perform this action. .. versionadded:: 2.4 diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 4f09e0a04..d51de610b 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -11,6 +11,192 @@ Changelog This page keeps a detailed human friendly rendering of what's new and changed in specific versions. +.. _vp2p4p0: + +v2.4.0 +------- + +New Features +~~~~~~~~~~~~~ + +- Add support for allowed contexts in app commands (:issue:`9760`). + - An "allowed context" is the location where an app command can be used. + - This is an internal change to decorators such as :func:`app_commands.guild_only` and :func:`app_commands.dm_only`. + - Add :func:`app_commands.private_channel_only`. + - Add :func:`app_commands.allowed_contexts`. + - Add :class:`app_commands.AppCommandContext`. + - Add :attr:`app_commands.Command.allowed_contexts`. + - Add :attr:`app_commands.AppCommand.allowed_contexts`. + - Add :attr:`app_commands.ContextMenu.allowed_contexts`. + +- Add support for user-installable apps (:issue:`9760`). + - Add :attr:`app_commands.Command.allowed_installs`. + - Add :attr:`app_commands.AppCommand.allowed_installs`. + - Add :attr:`app_commands.ContextMenu.allowed_installs`. + - Add :func:`app_commands.allowed_installs`. + - Add :func:`app_commands.guild_install`. + - Add :func:`app_commands.user_install`. + - Add :class:`app_commands.AppInstallationType`. + - Add :attr:`Interaction.context`. + - Add :meth:`Interaction.is_guild_integration`. + - Add :meth:`Interaction.is_user_integration`. + +- Add support for Polls (:issue:`9759`). + - Polls can be created using :class:`Poll` and the ``poll`` keyword-only parameter in various message sending methods. + - Add :class:`PollAnswer` and :class:`PollMedia`. + - Add :attr:`Intents.polls`, :attr:`Intents.guild_polls` and :attr:`Intents.dm_polls` intents. + - Add :meth:`Message.end_poll` method to end polls. + - Add new events, :func:`on_poll_vote_add`, :func:`on_poll_vote_remove`, :func:`on_raw_poll_vote_add`, and :func:`on_raw_poll_vote_remove`. + +- Voice handling has been completely rewritten to hopefully fix many bugs (:issue:`9525`, :issue:`9528`, :issue:`9536`, :issue:`9572`, :issue:`9576`, :issue:`9596`, :issue:`9683`, :issue:`9699`, :issue:`9772`, etc.) +- Add :attr:`DMChannel.recipients` to get all recipients of a DM channel (:issue:`9760`). +- Add support for :attr:`RawReactionActionEvent.message_author_id`. +- Add support for :attr:`AuditLogAction.creator_monetization_request_created` and :attr:`AuditLogAction.creator_monetization_terms_accepted`. +- Add support for :class:`AttachmentFlags`, accessed via :attr:`Attachment.flags` (:issue:`9486`). +- Add support for :class:`RoleFlags`, accessed via :attr:`Role.flags` (:issue:`9485`). +- Add support for :attr:`ChannelType.media`, accessed via :meth:`ForumChannel.is_media`. +- Add various new permissions (:issue:`9501`, :issue:`9762`, :issue:`9759`, :issue:`9857`) + - Add :meth:`Permissions.events`. + - Add :attr:`Permissions.create_events`. + - Add :attr:`Permissions.view_creator_monetization_analytics`. + - Add :attr:`Permissions.send_polls` + - Add :attr:`Permissions.create_polls`. + - Add :attr:`Permissions.use_external_apps`. + +- Add shortcut for :attr:`CategoryChannel.forums`. +- Add encoder options to :meth:`VoiceClient.play` (:issue:`9527`). +- Add support for team member roles. + - Add :class:`TeamMemberRole`. + - Add :attr:`TeamMember.role`. + - Updated :attr:`Bot.owner_ids <.ext.commands.Bot.owner_ids>` to account for team roles. Team owners or developers are considered Bot owners. + +- Add optional attribute ``integration_type`` in :attr:`AuditLogEntry.extra` for ``kick`` or ``member_role_update`` actions. +- Add support for "dynamic" :class:`ui.Item` that let you parse state out of a ``custom_id`` using regex. + - In order to use this, you must subclass :class:`ui.DynamicItem`. + - This is an alternative to persistent views. + - Add :meth:`Client.add_dynamic_items`. + - Add :meth:`Client.remove_dynamic_items`. + - Add :meth:`ui.Item.interaction_check`. + - Check the :resource:`dynamic_counter example ` for more information. + +- Add support for reading burst reactions. The API does not support sending them as of currently. + - Add :attr:`Reaction.normal_count`. + - Add :attr:`Reaction.burst_count`. + - Add :attr:`Reaction.me_burst`. + +- Add support for default values on select menus (:issue:`9577`). + - Add :class:`SelectDefaultValue`. + - Add :class:`SelectDefaultValueType`. + - Add a ``default_values`` attribute to each specialised select menu. + +- Add ``scheduled_event`` parameter for :meth:`StageChannel.create_instance` (:issue:`9595`). +- Add support for auto mod members (:issue:`9328`). + - Add ``type`` keyword argument to :class:`AutoModRuleAction`. + - Add :attr:`AutoModTrigger.mention_raid_protection`. + - Add :attr:`AutoModRuleTriggerType.member_profile`. + - Add :attr:`AutoModRuleEventType.member_update`. + - Add :attr:`AutoModRuleActionType.block_member_interactions`. + +- Add support for premium app integrations (:issue:`9453`). + - Add multiple SKU and entitlement related classes, e.g. :class:`SKU`, :class:`Entitlement`, :class:`SKUFlags`. + - Add multiple enums, e.g. :class:`SKUType`, :class:`EntitlementType`, :class:`EntitlementOwnerType`. + - Add :meth:`Client.fetch_skus` and :meth:`Client.fetch_entitlement` to fetch from the API. + - Add :meth:`Client.create_entitlement` to create entitlements. + - Add :attr:`Client.entitlements`. + - Add :attr:`Interaction.entitlement_sku_ids`. + - Add :attr:`Interaction.entitlements`. + - Add :attr:`ButtonStyle.premium` and :attr:`ui.Button.sku_id` to send a button asking the user to buy an SKU (:issue:`9845`). + - Add support for one time purchase (:issue:`9803`). + +- Add support for editing application info (:issue:`9610`). + - Add :attr:`AppInfo.interactions_endpoint_url`. + - Add :attr:`AppInfo.redirect_uris`. + - Add :meth:`AppInfo.edit`. + +- Add support for getting/fetching threads from :class:`Message` (:issue:`9665`). + - Add :attr:`PartialMessage.thread`. + - Add :attr:`Message.thread`. + - Add :meth:`Message.fetch_thread`. + +- Add support for platform and assets to activities (:issue:`9677`). + - Add :attr:`Activity.platform`. + - Add :attr:`Game.platform`. + - Add :attr:`Game.assets`. + +- Add support for suppressing embeds in an interaction response (:issue:`9678`). +- Add support for adding forum thread tags via webhook (:issue:`9680`) and (:issue:`9783`). +- Add support for guild incident message types (:issue:`9686`). +- Add :attr:`Locale.latin_american_spanish` (:issue:`9689`). +- Add support for setting voice channel status (:issue:`9603`). +- Add a shard connect timeout parameter to :class:`AutoShardedClient`. +- Add support for guild incidents (:issue:`9590`). + - Updated :meth:`Guild.edit` with ``invites_disabled_until`` and ``dms_disabled_until`` parameters. + - Add :attr:`Guild.invites_paused_until`. + - Add :attr:`Guild.dms_paused_until`. + - Add :meth:`Guild.invites_paused`. + - Add :meth:`Guild.dms_paused`. + +- Add support for :attr:`abc.User.avatar_decoration` (:issue:`9343`). +- Add support for GIF stickers (:issue:`9737`). +- Add support for updating :class:`ClientUser` banners (:issue:`9752`). +- Add support for bulk banning members via :meth:`Guild.bulk_ban`. +- Add ``reason`` keyword argument to :meth:`Thread.delete` (:issue:`9804`). +- Add :attr:`AppInfo.approximate_guild_count` (:issue:`9811`). +- Add support for :attr:`Message.interaction_metadata` (:issue:`9817`). +- Add support for differing :class:`Invite` types (:issue:`9682`). +- Add support for reaction types to raw and non-raw models (:issue:`9836`). +- |tasks| Add ``name`` parameter to :meth:`~ext.tasks.loop` to name the internal :class:`asyncio.Task`. +- |commands| Add fallback behaviour to :class:`~ext.commands.CurrentGuild`. +- |commands| Add logging for errors that occur during :meth:`~ext.commands.Cog.cog_unload`. +- |commands| Add support for :class:`typing.NewType` and ``type`` keyword type aliases (:issue:`9815`). + - Also supports application commands. + +- |commands| Add support for positional-only flag parameters (:issue:`9805`). +- |commands| Add support for channel URLs in ChannelConverter related classes (:issue:`9799`). + + +Bug Fixes +~~~~~~~~~~ + +- Fix emoji and sticker cache being populated despite turning the intent off. +- Fix outstanding chunk requests when receiving a gateway READY event not being cleared (:issue:`9571`). +- Fix escape behaviour for lists and headers in :meth:`~utils.escape_markdown`. +- Fix alias value for :attr:`Intents.auto_moderation` (:issue:`9524`). +- Fixes and improvements for :class:`FFmpegAudio` and all related subclasses (:issue:`9528`). +- Fix :meth:`Template.source_guild` attempting to resolve from cache (:issue:`9535`). +- Fix :exc:`IndexError` being raised instead of :exc:`ValueError` when calling :meth:`Colour.from_str` with an empty string (:issue:`9540`). +- Fix :meth:`View.from_message` not correctly creating the varying :class:`ui.Select` types (:issue:`9559`). +- Fix logging with autocomplete exceptions, which were previously suppressed. +- Fix possible error in voice cleanup logic (:issue:`9572`). +- Fix possible :exc:`AttributeError` during :meth:`app_commands.CommandTree.sync` when a command is regarded as 'too large'. +- Fix possible :exc:`TypeError` if a :class:`app_commands.Group` did not have a name set (:issue:`9581`). +- Fix possible bad voice state where you move to a voice channel with missing permissions (:issue:`9596`). +- Fix websocket reaching an error state due to received error payload (:issue:`9561`). +- Fix handling of :class:`AuditLogDiff` when relating to auto mod triggers (:issue:`9622`). +- Fix race condition in voice logic relating to disconnect and connect (:issue:`9683`). +- Use the :attr:`Interaction.user` guild as a fallback for :attr:`Interaction.guild` if not available. +- Fix restriction on auto moderation audit log ID range. +- Fix check for maximum number of children per :class:`ui.View`. +- Fix comparison between :class:`Object` classes with a ``type`` set. +- Fix handling of an enum in :meth:`AutoModRule.edit` (:issue:`9798`). +- Fix handling of :meth:`Client.close` within :meth:`Client.__aexit__` (:issue:`9769`). +- Fix channel deletion not evicting related threads from cache (:issue:`9796`). +- Fix bug with cache superfluously incrementing role positions (:issue:`9853`). +- Fix ``exempt_channels`` not being passed along in :meth:`Guild.create_automod_rule` (:issue:`9861`). +- Fix :meth:`abc.GuildChannel.purge` failing on single-message delete mode if the message was deleted (:issue:`9830`, :issue:`9863`). +- |commands| Fix localization support for :class:`~ext.commands.HybridGroup` fallback. +- |commands| Fix nested :class:`~ext.commands.HybridGroup`'s inserting manual app commands. +- |commands| Fix an issue where :class:`~ext.commands.HybridGroup` wrapped instances would be out of sync. +- |commands| Fix :class:`~ext.commands.HelpCommand` defined checks not carrying over during copy (:issue:`9843`). + +Miscellaneous +~~~~~~~~~~~~~~ + +- Additional documentation added for logging capabilities. +- Performance increases of constructing :class:`Permissions` using keyword arguments. +- Improve ``__repr__`` of :class:`SyncWebhook` and :class:`Webhook` (:issue:`9764`). +- Change internal thread names to be consistent (:issue:`9538`). + .. _vp2p3p2: v2.3.2