From ee6d9fcb5e0e8deab871dadaa0d6c2299b5d5e4d Mon Sep 17 00:00:00 2001 From: snipe <72265661+notsniped@users.noreply.github.com> Date: Fri, 31 Mar 2023 20:09:46 +0530 Subject: [PATCH] Update Pycord from 2.2.2 to 2.4.1 This should fix some issues with Discord embeds not returning correctly. --- discord/__init__.py | 23 +- discord/_typed_dict.py | 37 ++ discord/_version.py | 156 ++++++ discord/abc.py | 64 ++- discord/activity.py | 68 +-- discord/appinfo.py | 8 +- discord/application_role_connection.py | 128 +++++ discord/asset.py | 6 +- discord/audit_logs.py | 14 +- discord/automod.py | 100 +++- discord/bot.py | 46 +- discord/channel.py | 500 ++++++++++++++----- discord/client.py | 97 +++- discord/cog.py | 47 +- discord/colour.py | 91 ++-- discord/commands/context.py | 36 +- discord/commands/core.py | 64 ++- discord/commands/options.py | 38 +- discord/commands/permissions.py | 36 +- discord/components.py | 43 +- discord/embeds.py | 7 +- discord/emoji.py | 15 +- discord/enums.py | 190 +++++-- discord/errors.py | 14 +- discord/ext/bridge/bot.py | 15 + discord/ext/bridge/context.py | 2 +- discord/ext/bridge/core.py | 70 ++- discord/ext/commands/bot.py | 9 +- discord/ext/commands/context.py | 12 +- discord/ext/commands/cooldowns.py | 7 +- discord/ext/commands/core.py | 37 +- discord/ext/commands/errors.py | 6 +- discord/ext/commands/flags.py | 24 +- discord/ext/commands/help.py | 32 +- discord/ext/commands/view.py | 5 +- discord/ext/pages/pagination.py | 286 ++++++----- discord/ext/tasks/__init__.py | 27 +- discord/flags.py | 42 +- discord/gateway.py | 31 +- discord/guild.py | 114 +++-- discord/http.py | 60 ++- discord/integrations.py | 4 +- discord/interactions.py | 34 +- discord/invite.py | 16 +- discord/iterators.py | 1 - discord/member.py | 41 +- discord/message.py | 119 +++-- discord/object.py | 10 +- discord/opus.py | 12 +- discord/partial_emoji.py | 29 +- discord/permissions.py | 11 +- discord/player.py | 8 +- discord/raw_models.py | 95 +++- discord/reaction.py | 2 +- discord/role.py | 30 +- discord/scheduled_events.py | 6 +- discord/shard.py | 16 +- discord/stage_instance.py | 7 +- discord/state.py | 116 +++-- discord/sticker.py | 16 +- discord/team.py | 4 +- discord/template.py | 2 +- discord/threads.py | 74 ++- discord/types/activity.py | 19 +- discord/types/appinfo.py | 40 +- discord/types/application_role_connection.py | 40 ++ discord/types/audit_log.py | 14 +- discord/types/automod.py | 19 +- discord/types/channel.py | 75 +-- discord/types/components.py | 61 +-- discord/types/embed.py | 21 +- discord/types/emoji.py | 9 +- discord/types/guild.py | 107 ++-- discord/types/integration.py | 10 +- discord/types/interactions.py | 101 ++-- discord/types/invite.py | 43 +- discord/types/message.py | 61 +-- discord/types/raw_models.py | 79 +-- discord/types/role.py | 10 +- discord/types/sticker.py | 17 +- discord/types/threads.py | 26 +- discord/types/user.py | 7 +- discord/types/voice.py | 19 +- discord/types/webhook.py | 37 +- discord/types/widget.py | 17 +- discord/ui/button.py | 20 +- discord/ui/input_text.py | 16 +- discord/ui/item.py | 2 +- discord/ui/modal.py | 9 +- discord/ui/select.py | 347 ++++++++++++- discord/ui/view.py | 8 +- discord/user.py | 41 +- discord/utils.py | 95 +++- discord/voice_client.py | 22 +- discord/webhook/async_.py | 26 +- discord/webhook/sync.py | 12 +- discord/welcome_screen.py | 9 +- discord/widget.py | 17 +- 98 files changed, 3256 insertions(+), 1460 deletions(-) create mode 100644 discord/_typed_dict.py create mode 100644 discord/_version.py create mode 100644 discord/application_role_connection.py create mode 100644 discord/types/application_role_connection.py diff --git a/discord/__init__.py b/discord/__init__.py index 62531cd5..1b73d1ba 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -12,16 +12,22 @@ __author__ = "Pycord Development" __license__ = "MIT" __copyright__ = "Copyright 2015-2021 Rapptz & Copyright 2021-present Pycord Development" -__version__ = "2.2.2" __path__ = __import__("pkgutil").extend_path(__path__, __name__) import logging -from typing import Literal, NamedTuple + +# We need __version__ to be imported first +# isort: off +from ._version import * + +# isort: on + from . import abc, opus, sinks, ui, utils from .activity import * from .appinfo import * +from .application_role_connection import * from .asset import * from .audit_logs import * from .automod import * @@ -66,17 +72,4 @@ from .welcome_screen import * from .widget import * - -class VersionInfo(NamedTuple): - major: int - minor: int - micro: int - releaselevel: Literal["alpha", "beta", "candidate", "final"] - serial: int - - -version_info: VersionInfo = VersionInfo( - major=2, minor=2, micro=2, releaselevel="final", serial=0 -) - logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/discord/_typed_dict.py b/discord/_typed_dict.py new file mode 100644 index 00000000..de90a285 --- /dev/null +++ b/discord/_typed_dict.py @@ -0,0 +1,37 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +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. +""" +import sys + +# PEP 655 Required and NotRequired were added in python 3.11. This file is simply a +# shortcut import, so we don't have to repeat this import logic across files. +if sys.version_info >= (3, 11): + from typing import NotRequired, Required, TypedDict +else: + from typing_extensions import NotRequired, Required, TypedDict + +__all__ = ( + "Required", + "NotRequired", + "TypedDict", +) diff --git a/discord/_version.py b/discord/_version.py new file mode 100644 index 00000000..c58c8baa --- /dev/null +++ b/discord/_version.py @@ -0,0 +1,156 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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 + +import datetime +import re +import warnings +from importlib.metadata import PackageNotFoundError, version + +from ._typed_dict import TypedDict + +__all__ = ("__version__", "VersionInfo", "version_info") + +from typing import Literal, NamedTuple + +from .utils import deprecated + +try: + __version__ = version("py-cord") +except PackageNotFoundError: + # Package is not installed + try: + from setuptools_scm import get_version # type: ignore[import] + + __version__ = get_version() + except ImportError: + # setuptools_scm is not installed + __version__ = "0.0.0" + warnings.warn( + ( + "Package is not installed, and setuptools_scm is not installed. " + f"As a fallback, {__name__}.__version__ will be set to {__version__}" + ), + RuntimeWarning, + stacklevel=2, + ) + + +class AdvancedVersionInfo(TypedDict): + serial: int + build: int | None + commit: str | None + date: datetime.date | None + + +class VersionInfo(NamedTuple): + major: int + minor: int + micro: int + releaselevel: Literal["alpha", "beta", "candidate", "final"] + + # We can't set instance attributes on a NamedTuple, so we have to use a + # global variable to store the advanced version info. + @property + def advanced(self) -> AdvancedVersionInfo: + return _advanced + + @advanced.setter + def advanced(self, value: object) -> None: + global _advanced + _advanced = value + + @property + @deprecated("releaselevel", "2.4") + def release_level(self) -> Literal["alpha", "beta", "candidate", "final"]: + return self.releaselevel + + @property + @deprecated('.advanced["serial"]', "2.4") + def serial(self) -> int: + return self.advanced["serial"] + + @property + @deprecated('.advanced["build"]', "2.4") + def build(self) -> int | None: + return self.advanced["build"] + + @property + @deprecated('.advanced["commit"]', "2.4") + def commit(self) -> str | None: + return self.advanced["commit"] + + @property + @deprecated('.advanced["date"]', "2.4") + def date(self) -> datetime.date | None: + return self.advanced["date"] + + +version_regex = re.compile( + r"^(?P\d+)(?:\.(?P\d+))?(?:\.(?P\d+))?" + r"(?:(?Prc|a|b)(?P\d+))?" + r"(?:\.dev(?P\d+))?" + r"(?:\+(?:(?:g(?P[a-fA-F0-9]{4,40})(?:\.d(?P\d{4}\d{2}\d{2})|))|d(?P\d{4}\d{2}\d{2})))?$" +) +version_match = version_regex.match(__version__) +if version_match is None: + raise RuntimeError(f"Invalid version string: {__version__}") +raw_info = version_match.groupdict() + +level_info: Literal["alpha", "beta", "candidate", "final"] + +if raw_info["level"] == "a": + level_info = "alpha" +elif raw_info["level"] == "b": + level_info = "beta" +elif raw_info["level"] == "rc": + level_info = "candidate" +elif raw_info["level"] is None: + level_info = "final" +else: + raise RuntimeError("Invalid release level") + +if (raw_date := raw_info["date"] or raw_info["date1"]) is not None: + date_info = datetime.date( + int(raw_date[:4]), + int(raw_date[4:6]), + int(raw_date[6:]), + ) +else: + date_info = None + +version_info: VersionInfo = VersionInfo( + major=int(raw_info["major"] or 0) or None, + minor=int(raw_info["minor"] or 0) or None, + micro=int(raw_info["patch"] or 0) or None, + releaselevel=level_info, +) + +_advanced = AdvancedVersionInfo( + serial=raw_info["serial"], + build=int(raw_info["build"] or 0) or None, + commit=raw_info["commit"], + date=date_info, +) diff --git a/discord/abc.py b/discord/abc.py index ce0806bb..4ab10409 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -229,12 +229,12 @@ class User(Snowflake, Protocol): @property def display_name(self) -> str: - """:class:`str`: Returns the user's display name.""" + """Returns the user's display name.""" raise NotImplementedError @property def mention(self) -> str: - """:class:`str`: Returns a string that allows you to mention the given user.""" + """Returns a string that allows you to mention the given user.""" raise NotImplementedError @@ -400,6 +400,26 @@ async def _edit( except KeyError: pass + try: + options["default_thread_rate_limit_per_user"] = options.pop( + "default_thread_slowmode_delay" + ) + except KeyError: + pass + + try: + if options.pop("require_tag"): + options["flags"] = ChannelFlags.require_tag.flag + except KeyError: + pass + + try: + options["available_tags"] = [ + tag.to_dict() for tag in options.pop("available_tags") + ] + except KeyError: + pass + try: rtc_region = options.pop("rtc_region") except KeyError: @@ -449,7 +469,8 @@ async def _edit( for target, perm in overwrites.items(): if not isinstance(perm, PermissionOverwrite): raise InvalidArgument( - f"Expected PermissionOverwrite received {perm.__class__.__name__}" + "Expected PermissionOverwrite received" + f" {perm.__class__.__name__}" ) allow, deny = perm.pair() @@ -457,9 +478,11 @@ async def _edit( "allow": allow.value, "deny": deny.value, "id": target.id, - "type": _Overwrites.ROLE - if isinstance(target, Role) - else _Overwrites.MEMBER, + "type": ( + _Overwrites.ROLE + if isinstance(target, Role) + else _Overwrites.MEMBER + ), } perms.append(payload) @@ -506,7 +529,7 @@ def _fill_overwrites(self, data: GuildChannelPayload) -> None: @property def changed_roles(self) -> list[Role]: - """List[:class:`~discord.Role`]: Returns a list of roles that have been overridden from + """Returns a list of roles that have been overridden from their default values in the :attr:`~discord.Guild.roles` attribute. """ ret = [] @@ -523,12 +546,12 @@ def changed_roles(self) -> list[Role]: @property def mention(self) -> str: - """:class:`str`: The string that allows you to mention the channel.""" + """The string that allows you to mention the channel.""" return f"<#{self.id}>" @property def jump_url(self) -> str: - """:class:`str`: Returns a URL that allows the client to jump to the channel. + """Returns a URL that allows the client to jump to the channel. .. versionadded:: 2.0 """ @@ -536,7 +559,7 @@ def jump_url(self) -> str: @property def created_at(self) -> datetime: - """:class:`datetime.datetime`: Returns the channel's creation time in UTC.""" + """Returns the channel's creation time in UTC.""" return utils.snowflake_time(self.id) def overwrites_for(self, obj: Role | User) -> PermissionOverwrite: @@ -605,7 +628,7 @@ def overwrites(self) -> dict[Role | Member, PermissionOverwrite]: @property def category(self) -> CategoryChannel | None: - """Optional[:class:`~discord.CategoryChannel`]: The category this channel belongs to. + """The category this channel belongs to. If there is no category then this is ``None``. """ @@ -613,7 +636,7 @@ def category(self) -> CategoryChannel | None: @property def permissions_synced(self) -> bool: - """:class:`bool`: Whether the permissions for this channel are synced with the + """Whether the permissions for this channel are synced with the category it belongs to. If there is no category then this is ``False``. @@ -1300,6 +1323,7 @@ async def send( mention_author: bool = ..., view: View = ..., suppress: bool = ..., + silent: bool = ..., ) -> Message: ... @@ -1319,6 +1343,7 @@ async def send( mention_author: bool = ..., view: View = ..., suppress: bool = ..., + silent: bool = ..., ) -> Message: ... @@ -1338,6 +1363,7 @@ async def send( mention_author: bool = ..., view: View = ..., suppress: bool = ..., + silent: bool = ..., ) -> Message: ... @@ -1357,6 +1383,7 @@ async def send( mention_author: bool = ..., view: View = ..., suppress: bool = ..., + silent: bool = ..., ) -> Message: ... @@ -1377,6 +1404,7 @@ async def send( mention_author=None, view=None, suppress=None, + silent=None, ): """|coro| @@ -1450,6 +1478,10 @@ async def send( .. versionadded:: 2.0 suppress: :class:`bool` Whether to suppress embeds for the message. + slient: :class:`bool` + Whether to suppress push and desktop notifications for the message. + + .. versionadded:: 2.4 Returns ------- @@ -1489,7 +1521,10 @@ async def send( ) embeds = [embed.to_dict() for embed in embeds] - flags = MessageFlags.suppress_embeds if suppress else MessageFlags.DEFAULT_VALUE + flags = MessageFlags( + suppress_embeds=bool(suppress), + suppress_notifications=bool(silent), + ).value if stickers is not None: stickers = [sticker.id for sticker in stickers] @@ -1512,7 +1547,8 @@ async def send( reference = reference.to_message_reference_dict() except AttributeError: raise InvalidArgument( - "reference parameter must be Message, MessageReference, or PartialMessage" + "reference parameter must be Message, MessageReference, or" + " PartialMessage" ) from None if view: diff --git a/discord/activity.py b/discord/activity.py index e6499c58..52e7df08 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -124,7 +124,7 @@ def __init__(self, **kwargs): @property def created_at(self) -> datetime.datetime | None: - """Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC. + """When the user started doing this activity in UTC. .. versionadded:: 1.3 """ @@ -279,7 +279,7 @@ def to_dict(self) -> dict[str, Any]: @property def start(self) -> datetime.datetime | None: - """Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable.""" + """When the user started doing this activity in UTC, if applicable.""" try: timestamp = self.timestamps["start"] / 1000 except KeyError: @@ -289,7 +289,7 @@ def start(self) -> datetime.datetime | None: @property def end(self) -> datetime.datetime | None: - """Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable.""" + """When the user will stop doing this activity in UTC, if applicable.""" try: timestamp = self.timestamps["end"] / 1000 except KeyError: @@ -299,7 +299,7 @@ def end(self) -> datetime.datetime | None: @property def large_image_url(self) -> str | None: - """Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity if applicable.""" + """Returns a URL pointing to the large image asset of this activity if applicable.""" if self.application_id is None: return None @@ -312,7 +312,7 @@ def large_image_url(self) -> str | None: @property def small_image_url(self) -> str | None: - """Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity if applicable.""" + """Returns a URL pointing to the small image asset of this activity if applicable.""" if self.application_id is None: return None @@ -325,12 +325,12 @@ def small_image_url(self) -> str | None: @property def large_image_text(self) -> str | None: - """Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable.""" + """Returns the large image asset hover text of this activity if applicable.""" return self.assets.get("large_text", None) @property def small_image_text(self) -> str | None: - """Optional[:class:`str`]: Returns the small image asset hover text of this activity if applicable.""" + """Returns the small image asset hover text of this activity if applicable.""" return self.assets.get("small_text", None) @@ -385,7 +385,7 @@ def __init__(self, name: str, **extra): @property def type(self) -> ActivityType: - """:class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`. + """Returns the game's type. This is for compatibility with :class:`Activity`. It always returns :attr:`ActivityType.playing`. """ @@ -393,7 +393,7 @@ def type(self) -> ActivityType: @property def start(self) -> datetime.datetime | None: - """Optional[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable.""" + """When the user started playing this game in UTC, if applicable.""" if self._start: return datetime.datetime.fromtimestamp( self._start / 1000, tz=datetime.timezone.utc @@ -402,7 +402,7 @@ def start(self) -> datetime.datetime | None: @property def end(self) -> datetime.datetime | None: - """Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable.""" + """When the user will stop playing this game in UTC, if applicable.""" if self._end: return datetime.datetime.fromtimestamp( self._end / 1000, tz=datetime.timezone.utc @@ -497,7 +497,7 @@ def __init__(self, *, name: str | None, url: str, **extra: Any): @property def type(self) -> ActivityType: - """:class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`. + """Returns the game's type. This is for compatibility with :class:`Activity`. It always returns :attr:`ActivityType.streaming`. """ @@ -510,8 +510,8 @@ def __repr__(self) -> str: return f"" @property - def twitch_name(self): - """Optional[:class:`str`]: If provided, the twitch name of the user streaming. + def twitch_name(self) -> str | None: + """If provided, the twitch name of the user streaming. This corresponds to the ``large_image`` key of the :attr:`Streaming.assets` dictionary if it starts with ``twitch:``. Typically this is set by the Discord client. @@ -595,7 +595,7 @@ def __init__(self, **data): @property def type(self) -> ActivityType: - """:class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`. + """Returns the activity's type. This is for compatibility with :class:`Activity`. It always returns :attr:`ActivityType.listening`. """ @@ -603,7 +603,7 @@ def type(self) -> ActivityType: @property def created_at(self) -> datetime.datetime | None: - """Optional[:class:`datetime.datetime`]: When the user started listening in UTC. + """When the user started listening in UTC. .. versionadded:: 1.3 """ @@ -614,7 +614,7 @@ def created_at(self) -> datetime.datetime | None: @property def colour(self) -> Colour: - """:class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`. + """Returns the Spotify integration colour, as a :class:`Colour`. There is an alias for this named :attr:`color` """ @@ -622,7 +622,7 @@ def colour(self) -> Colour: @property def color(self) -> Colour: - """:class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`. + """Returns the Spotify integration colour, as a :class:`Colour`. There is an alias for this named :attr:`colour` """ @@ -643,7 +643,7 @@ def to_dict(self) -> dict[str, Any]: @property def name(self) -> str: - """:class:`str`: The activity's name. This will always return "Spotify".""" + """The activity's name. This will always return "Spotify".""" return "Spotify" def __eq__(self, other: Any) -> bool: @@ -664,21 +664,24 @@ def __str__(self) -> str: return "Spotify" def __repr__(self) -> str: - return f"" + return ( + "" + ) @property def title(self) -> str: - """:class:`str`: The title of the song being played.""" + """The title of the song being played.""" return self._details @property def artists(self) -> list[str]: - """List[:class:`str`]: The artists of the song being played.""" + """The artists of the song being played.""" return self._state.split("; ") @property def artist(self) -> str: - """:class:`str`: The artist of the song being played. + """The artist of the song being played. This does not attempt to split the artist information into multiple artists. Useful if there's only a single artist. @@ -687,12 +690,12 @@ def artist(self) -> str: @property def album(self) -> str: - """:class:`str`: The album that the song being played belongs to.""" + """The album that the song being played belongs to.""" return self._assets.get("large_text", "") @property def album_cover_url(self) -> str: - """:class:`str`: The album cover image URL from Spotify's CDN.""" + """The album cover image URL from Spotify's CDN.""" large_image = self._assets.get("large_image", "") if large_image[:8] != "spotify:": return "" @@ -701,12 +704,12 @@ def album_cover_url(self) -> str: @property def track_id(self) -> str: - """:class:`str`: The track ID used by Spotify to identify this song.""" + """The track ID used by Spotify to identify this song.""" return self._sync_id @property def track_url(self) -> str: - """:class:`str`: The track URL to listen on Spotify. + """The track URL to listen on Spotify. .. versionadded:: 2.0 """ @@ -714,26 +717,26 @@ def track_url(self) -> str: @property def start(self) -> datetime.datetime: - """:class:`datetime.datetime`: When the user started playing this song in UTC.""" + """When the user started playing this song in UTC.""" return datetime.datetime.fromtimestamp( self._timestamps["start"] / 1000, tz=datetime.timezone.utc ) @property def end(self) -> datetime.datetime: - """:class:`datetime.datetime`: When the user will stop playing this song in UTC.""" + """When the user will stop playing this song in UTC.""" return datetime.datetime.fromtimestamp( self._timestamps["end"] / 1000, tz=datetime.timezone.utc ) @property def duration(self) -> datetime.timedelta: - """:class:`datetime.timedelta`: The duration of the song being played.""" + """The duration of the song being played.""" return self.end - self.start @property def party_id(self) -> str: - """:class:`str`: The party ID of the listening party.""" + """The party ID of the listening party.""" return self._party.get("id", "") @@ -790,12 +793,13 @@ def __init__( self.emoji = emoji else: raise TypeError( - f"Expected str, PartialEmoji, or None, received {type(emoji)!r} instead." + "Expected str, PartialEmoji, or None, received" + f" {type(emoji)!r} instead." ) @property def type(self) -> ActivityType: - """:class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`. + """Returns the activity's type. This is for compatibility with :class:`Activity`. It always returns :attr:`ActivityType.custom`. """ diff --git a/discord/appinfo.py b/discord/appinfo.py index 9024b5e3..9554c96b 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -171,14 +171,14 @@ def __repr__(self) -> str: @property def icon(self) -> Asset | None: - """Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any.""" + """Retrieves the application's icon asset, if any.""" if self._icon is None: return None return Asset._from_icon(self._state, self.id, self._icon, path="app") @property def cover_image(self) -> Asset | None: - """Optional[:class:`.Asset`]: Retrieves the cover image on a store embed, if any. + """Retrieves the cover image on a store embed, if any. This is only available if the application is a game sold on Discord. """ @@ -188,7 +188,7 @@ def cover_image(self) -> Asset | None: @property def guild(self) -> Guild | None: - """Optional[:class:`Guild`]: If this application is a game sold on Discord, + """If this application is a game sold on Discord, this field will be the guild to which it has been linked. .. versionadded:: 1.3 @@ -253,7 +253,7 @@ def __repr__(self) -> str: @property def icon(self) -> Asset | None: - """Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any.""" + """Retrieves the application's icon asset, if any.""" if self._icon is None: return None return Asset._from_icon(self._state, self.id, self._icon, path="app") diff --git a/discord/application_role_connection.py b/discord/application_role_connection.py new file mode 100644 index 00000000..dc4ef01d --- /dev/null +++ b/discord/application_role_connection.py @@ -0,0 +1,128 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +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 TYPE_CHECKING + +from .enums import ApplicationRoleConnectionMetadataType, try_enum +from .utils import MISSING + +if TYPE_CHECKING: + from .types.application_role_connection import ( + ApplicationRoleConnectionMetadata as ApplicationRoleConnectionMetadataPayload, + ) + +__all__ = ("ApplicationRoleConnectionMetadata",) + + +class ApplicationRoleConnectionMetadata: + r"""Represents role connection metadata for a Discord application. + + .. versionadded:: 2.4 + + Parameters + ---------- + type: :class:`ApplicationRoleConnectionMetadataType` + The type of metadata value. + key: :class:`str` + The key for this metadata field. + May only be the ``a-z``, ``0-9``, or ``_`` characters, with a maximum of 50 characters. + name: :class:`str` + The name for this metadata field. Maximum 100 characters. + description: :class:`str` + The description for this metadata field. Maximum 200 characters. + name_localizations: Optional[Dict[:class:`str`, :class:`str`]] + The name localizations for this metadata field. The values of this should be ``"locale": "name"``. + See `here `_ for a list of valid locales. + description_localizations: Optional[Dict[:class:`str`, :class:`str`]] + The description localizations for this metadata field. The values of this should be ``"locale": "name"``. + See `here `_ for a list of valid locales. + """ + + __slots__ = ( + "type", + "key", + "name", + "description", + "name_localizations", + "description_localizations", + ) + + def __init__( + self, + *, + type: ApplicationRoleConnectionMetadataType, + key: str, + name: str, + description: str, + name_localizations: dict[str, str] = MISSING, + description_localizations: dict[str, str] = MISSING, + ): + self.type: ApplicationRoleConnectionMetadataType = type + self.key: str = key + self.name: str = name + self.name_localizations: dict[str, str] = name_localizations + self.description: str = description + self.description_localizations: dict[str, str] = description_localizations + + def __repr__(self): + return ( + "" + ) + + def __str__(self): + return self.name + + @classmethod + def from_dict( + cls, data: ApplicationRoleConnectionMetadataPayload + ) -> ApplicationRoleConnectionMetadata: + return cls( + type=try_enum(ApplicationRoleConnectionMetadataType, data["type"]), + key=data["key"], + name=data["name"], + description=data["description"], + name_localizations=data.get("name_localizations"), + description_localizations=data.get("description_localizations"), + ) + + def to_dict(self) -> ApplicationRoleConnectionMetadataPayload: + data = { + "type": self.type.value, + "key": self.key, + "name": self.name, + "description": self.description, + } + if self.name_localizations is not MISSING: + data["name_localizations"] = self.name_localizations + if self.description_localizations is not MISSING: + data["description_localizations"] = self.description_localizations + return data diff --git a/discord/asset.py b/discord/asset.py index 9cfcb928..fc11f89b 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -288,16 +288,16 @@ def __hash__(self): @property def url(self) -> str: - """:class:`str`: Returns the underlying URL of the asset.""" + """Returns the underlying URL of the asset.""" return self._url @property def key(self) -> str: - """:class:`str`: Returns the identifying key of the asset.""" + """Returns the identifying key of the asset.""" return self._key def is_animated(self) -> bool: - """:class:`bool`: Returns whether the asset is animated.""" + """Returns whether the asset is animated.""" return self._animated def replace( diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 892c7e89..c427534e 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -109,9 +109,9 @@ def _transform_overwrites( ow_type = elem["type"] ow_id = int(elem["id"]) target = None - if ow_type == "0": + if ow_type == 0: target = entry.guild.get_role(ow_id) - elif ow_type == "1": + elif ow_type == 1: target = entry._get_member(ow_id) if target is None: @@ -528,7 +528,7 @@ def __repr__(self) -> str: @utils.cached_property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the entry's creation time in UTC.""" + """Returns the entry's creation time in UTC.""" return utils.snowflake_time(self.id) @utils.cached_property @@ -557,24 +557,24 @@ def target( @utils.cached_property def category(self) -> enums.AuditLogActionCategory: - """Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable.""" + """The category of the action, if applicable.""" return self.action.category @utils.cached_property def changes(self) -> AuditLogChanges: - """:class:`AuditLogChanges`: The list of changes this entry has.""" + """The list of changes this entry has.""" obj = AuditLogChanges(self, self._changes, state=self._state) del self._changes return obj @utils.cached_property def before(self) -> AuditLogDiff: - """:class:`AuditLogDiff`: The target's prior state.""" + """The target's prior state.""" return self.changes.before @utils.cached_property def after(self) -> AuditLogDiff: - """:class:`AuditLogDiff`: The target's subsequent state.""" + """The target's subsequent state.""" return self.changes.after def _convert_target_guild(self, target_id: int) -> Guild: diff --git a/discord/automod.py b/discord/automod.py index 02fbd4ab..5f091d0f 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -39,7 +39,12 @@ from .mixins import Hashable from .object import Object -__all__ = ("AutoModRule",) +__all__ = ( + "AutoModRule", + "AutoModAction", + "AutoModActionMetadata", + "AutoModTriggerMetadata", +) if TYPE_CHECKING: from .abc import Snowflake @@ -152,7 +157,7 @@ def __init__(self, action_type: AutoModActionType, metadata: AutoModActionMetada def to_dict(self) -> dict: return { "type": self.type.value, - "metadata": self.metadata, + "metadata": self.metadata.to_dict(), } @classmethod @@ -167,35 +172,76 @@ def __repr__(self) -> str: class AutoModTriggerMetadata: - """Represents a rule's trigger metadata. - - Depending on the trigger type, different attributes will be used. + r"""Represents a rule's trigger metadata, defining additional data used to determine when a rule triggers. + + Depending on the trigger type, different metadata attributes will be used: + + +-----------------------------+--------------------------------------------------------------------------------+ + | Attribute | Trigger Types | + +=============================+================================================================================+ + | :attr:`keyword_filter` | :attr:`AutoModTriggerType.keyword` | + +-----------------------------+--------------------------------------------------------------------------------+ + | :attr:`regex_patterns` | :attr:`AutoModTriggerType.keyword` | + +-----------------------------+--------------------------------------------------------------------------------+ + | :attr:`presets` | :attr:`AutoModTriggerType.keyword_preset` | + +-----------------------------+--------------------------------------------------------------------------------+ + | :attr:`allow_list` | :attr:`AutoModTriggerType.keyword`\, :attr:`AutoModTriggerType.keyword_preset` | + +-----------------------------+--------------------------------------------------------------------------------+ + | :attr:`mention_total_limit` | :attr:`AutoModTriggerType.mention_spam` | + +-----------------------------+--------------------------------------------------------------------------------+ + + Each attribute has limits that may change based on the trigger type. + See `here `_ + for information on attribute limits. .. versionadded:: 2.0 Attributes ---------- keyword_filter: List[:class:`str`] - A list of substrings to filter. Only for triggers of type :attr:`AutoModTriggerType.keyword`. + A list of substrings to filter. + + regex_patterns: List[:class:`str`] + A list of regex patterns to filter using Rust-flavored regex, which is not + fully compatible with regex syntax supported by the builtin `re` module. + + .. versionadded:: 2.4 + presets: List[:class:`AutoModKeywordPresetType`] - A list of keyword presets to filter. Only for triggers of type :attr:`AutoModTriggerType.keyword_preset`. - """ + A list of preset keyword sets to filter. - # maybe add a table of action types and attributes? - # wording for presets could change + allow_list: List[:class:`str`] + A list of substrings to allow, overriding keyword and regex matches. + + .. versionadded:: 2.4 + + mention_total_limit: :class:`int` + The total number of unique role and user mentions allowed. + + .. versionadded:: 2.4 + """ __slots__ = ( "keyword_filter", + "regex_patterns", "presets", + "allow_list", + "mention_total_limit", ) def __init__( self, keyword_filter: list[str] = MISSING, + regex_patterns: list[str] = MISSING, presets: list[AutoModKeywordPresetType] = MISSING, + allow_list: list[str] = MISSING, + mention_total_limit: int = MISSING, ): self.keyword_filter = keyword_filter + self.regex_patterns = regex_patterns self.presets = presets + self.allow_list = allow_list + self.mention_total_limit = mention_total_limit def to_dict(self) -> dict: data = {} @@ -203,9 +249,18 @@ def to_dict(self) -> dict: if self.keyword_filter is not MISSING: data["keyword_filter"] = self.keyword_filter + if self.regex_patterns is not MISSING: + data["regex_patterns"] = self.regex_patterns + if self.presets is not MISSING: data["presets"] = [wordset.value for wordset in self.presets] + if self.allow_list is not MISSING: + data["allow_list"] = self.allow_list + + if self.mention_total_limit is not MISSING: + data["mention_total_limit"] = self.mention_total_limit + return data @classmethod @@ -215,17 +270,29 @@ def from_dict(cls, data: AutoModTriggerMetadataPayload): if (keyword_filter := data.get("keyword_filter")) is not None: kwargs["keyword_filter"] = keyword_filter + if (regex_patterns := data.get("regex_patterns")) is not None: + kwargs["regex_patterns"] = regex_patterns + if (presets := data.get("presets")) is not None: kwargs["presets"] = [ try_enum(AutoModKeywordPresetType, wordset) for wordset in presets ] + if (allow_list := data.get("allow_list")) is not None: + kwargs["allow_list"] = allow_list + + if (mention_total_limit := data.get("mention_total_limit")) is not None: + kwargs["mention_total_limit"] = mention_total_limit + return cls(**kwargs) def __repr__(self) -> str: repr_attrs = ( "keyword_filter", + "regex_patterns", "presets", + "allow_list", + "mention_total_limit", ) inner = [] @@ -334,19 +401,19 @@ def __str__(self) -> str: @cached_property def guild(self) -> Guild | None: - """Optional[:class:`Guild`]: The guild this rule belongs to.""" + """The guild this rule belongs to.""" return self._state._get_guild(self.guild_id) @cached_property def creator(self) -> Member | None: - """Optional[:class:`Member`]: The member who created this rule.""" + """The member who created this rule.""" if self.guild is None: return None return self.guild.get_member(self.creator_id) @cached_property def exempt_roles(self) -> list[Role | Object]: - """List[Union[:class:`Role`, :class:`Object`]]: The roles that are exempt + """The roles that are exempt from this rule. If a role is not found in the guild's cache, @@ -363,8 +430,7 @@ def exempt_roles(self) -> list[Role | Object]: def exempt_channels( self, ) -> list[TextChannel | ForumChannel | VoiceChannel | Object]: - """List[Union[Union[:class:`TextChannel`, :class:`ForumChannel`, :class:`VoiceChannel`], :class:`Object`]]: The - channels that are exempt from this rule. + """The channels that are exempt from this rule. If a channel is not found in the guild's cache, then it will be returned as an :class:`Object`. @@ -425,9 +491,9 @@ async def edit( The new actions to perform when the rule is triggered. enabled: :class:`bool` Whether this rule is enabled. - exempt_roles: List[:class:`Snowflake`] + exempt_roles: List[:class:`abc.Snowflake`] The roles that will be exempt from this rule. - exempt_channels: List[:class:`Snowflake`] + exempt_channels: List[:class:`abc.Snowflake`] The channels that will be exempt from this rule. reason: Optional[:class:`str`] The reason for editing this rule. Shows up in the audit log. diff --git a/discord/bot.py b/discord/bot.py index 697a9be1..b6c35390 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -179,7 +179,7 @@ def get_application_command( self, name: str, guild_ids: list[int] | None = None, - type: type[ApplicationCommand] = SlashCommand, + type: type[ApplicationCommand] = ApplicationCommand, ) -> ApplicationCommand | None: """Get a :class:`.ApplicationCommand` from the internal list of commands. @@ -189,23 +189,39 @@ def get_application_command( Parameters ---------- name: :class:`str` - The name of the command to get. + The qualified name of the command to get. guild_ids: List[:class:`int`] The guild ids associated to the command to get. type: Type[:class:`.ApplicationCommand`] - The type of the command to get. Defaults to :class:`.SlashCommand`. + The type of the command to get. Defaults to :class:`.ApplicationCommand`. Returns ------- Optional[:class:`.ApplicationCommand`] The command that was requested. If not found, returns ``None``. """ - - for command in self._application_commands.values(): + commands = self._application_commands.values() + for command in commands: if command.name == name and isinstance(command, type): if guild_ids is not None and command.guild_ids != guild_ids: return return command + elif (names := name.split())[0] == command.name and isinstance( + command, SlashCommandGroup + ): + while len(names) > 1: + command = get(commands, name=names.pop(0)) + if not isinstance(command, SlashCommandGroup) or ( + guild_ids is not None and command.guild_ids != guild_ids + ): + return + commands = command.subcommands + command = get(commands, name=names.pop()) + if not isinstance(command, type) or ( + guild_ids is not None and command.guild_ids != guild_ids + ): + return + return command async def get_desynced_commands( self, @@ -259,6 +275,7 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: as_dict = cmd.to_dict() to_check = { "dm_permission": None, + "nsfw": None, "default_member_permissions": None, "name": None, "description": None, @@ -489,7 +506,8 @@ def register( if kwargs.pop("_log", True): if method == "bulk": _log.debug( - f"Bulk updating commands {[c['name'] for c in args[0]]} for guild {guild_id}" + f"Bulk updating commands {[c['name'] for c in args[0]]} for" + f" guild {guild_id}" ) # TODO: Find where "cmd" is defined elif method == "upsert": @@ -620,7 +638,8 @@ def register( ) if not cmd: raise ValueError( - f"Registered command {i['name']}, type {i.get('type')} not found in pending commands" + f"Registered command {i['name']}, type {i.get('type')} not found in" + " pending commands" ) cmd.id = i["id"] self._application_commands[cmd.id] = cmd @@ -645,7 +664,15 @@ async def sync_commands( register all commands. By default, this coroutine is called inside the :func:`.on_connect` event. If you choose to override the - :func:`.on_connect` event, then you should invoke this coroutine as well. + :func:`.on_connect` event, then you should invoke this coroutine as well such as the follwing: + + .. code-block:: python + + @bot.event + async def on_connect(): + if bot.auto_sync_commands: + await bot.sync_commands() + print(f"{bot.user.name} connected.") .. note:: If you remove all guild commands from a particular guild, the library may not be able to detect and update @@ -807,7 +834,6 @@ async def process_application_commands( if guild_id is None: await self.sync_commands() else: - await self.sync_commands(check_guilds=[guild_id]) return self._bot.dispatch("unknown_application_command", interaction) @@ -1500,7 +1526,7 @@ class Bot(BotBase, Client): .. versionadded:: 2.0 auto_sync_commands: :class:`bool` - Whether to automatically sync slash commands. This will call sync_commands in on_connect, and in + Whether to automatically sync slash commands. This will call :meth:`~.Bot.sync_commands` in :func:`discord.on_connect`, and in :attr:`.process_application_commands` if the command is not found. Defaults to ``True``. .. versionadded:: 2.0 diff --git a/discord/channel.py b/discord/channel.py index 73ca1fe2..2efca7be 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -36,6 +36,7 @@ ChannelType, EmbeddedActivity, InviteTarget, + SortOrder, StagePrivacyLevel, VideoQualityMode, VoiceRegion, @@ -48,6 +49,7 @@ from .iterators import ArchivedThreadIterator from .mixins import Hashable from .object import Object +from .partial_emoji import PartialEmoji, _EmojiTag from .permissions import PermissionOverwrite, Permissions from .stage_instance import StageInstance from .threads import Thread @@ -62,6 +64,7 @@ "GroupChannel", "PartialMessageable", "ForumChannel", + "ForumTag", ) if TYPE_CHECKING: @@ -69,12 +72,13 @@ from .guild import Guild from .guild import GuildChannel as GuildChannelType from .member import Member, VoiceState - from .message import Message, PartialMessage + from .message import EmojiInputType, Message, PartialMessage from .role import Role from .state import ConnectionState from .types.channel import CategoryChannel as CategoryChannelPayload from .types.channel import DMChannel as DMChannelPayload from .types.channel import ForumChannel as ForumChannelPayload + from .types.channel import ForumTag as ForumTagPayload from .types.channel import GroupDMChannel as GroupChannelPayload from .types.channel import StageChannel as StageChannelPayload from .types.channel import TextChannel as TextChannelPayload @@ -85,6 +89,96 @@ from .webhook import Webhook +class ForumTag(Hashable): + """Represents a forum tag that can be added to a thread inside a :class:`ForumChannel` + . + .. versionadded:: 2.3 + + .. 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 tag ID. + Note that if the object was created manually then this 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( + "emoji must be a Emoji, PartialEmoji, or str and not" + f" {emoji.__class__!r}" + ) + + def __repr__(self) -> str: + return ( + "" + ) + + def __str__(self) -> str: + return self.name + + @classmethod + def from_data(cls, *, state: ConnectionState, data: ForumTagPayload) -> ForumTag: + 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 + 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, + } | self.emoji._to_forum_tag_payload() + + if self.id: + payload["id"] = self.id + + return payload + + class _TextChannel(discord.abc.GuildChannel, Hashable): __slots__ = ( "name", @@ -100,6 +194,9 @@ class _TextChannel(discord.abc.GuildChannel, Hashable): "_type", "last_message_id", "default_auto_archive_duration", + "default_thread_slowmode_delay", + "default_sort_order", + "available_tags", "flags", ) @@ -142,6 +239,9 @@ def _update( self.default_auto_archive_duration: ThreadArchiveDuration = data.get( "default_auto_archive_duration", 1440 ) + self.default_thread_slowmode_delay: int | None = data.get( + "default_thread_rate_limit_per_user" + ) self.last_message_id: int | None = utils._get_as_snowflake( data, "last_message_id" ) @@ -150,7 +250,7 @@ def _update( @property def type(self) -> ChannelType: - """:class:`ChannelType`: The channel's Discord type.""" + """The channel's Discord type.""" return try_enum(ChannelType, self._type) @property @@ -168,12 +268,12 @@ def permissions_for(self, obj: Member | Role, /) -> Permissions: @property def members(self) -> list[Member]: - """List[:class:`Member`]: Returns all members that can see this channel.""" + """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[Thread]: - """List[:class:`Thread`]: Returns all the threads that you can see. + """Returns all the threads that you can see. .. versionadded:: 2.0 """ @@ -184,7 +284,7 @@ def threads(self) -> list[Thread]: ] def is_nsfw(self) -> bool: - """:class:`bool`: Checks if the channel is NSFW.""" + """Checks if the channel is NSFW.""" return self.nsfw @property @@ -212,97 +312,9 @@ def last_message(self) -> Message | None: else None ) - @overload - async def edit( - self, - *, - reason: str | None = ..., - name: str = ..., - topic: str = ..., - position: int = ..., - nsfw: bool = ..., - sync_permissions: bool = ..., - category: CategoryChannel | None = ..., - slowmode_delay: int = ..., - default_auto_archive_duration: ThreadArchiveDuration = ..., - type: ChannelType = ..., - overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., - ) -> TextChannel | None: - ... - - @overload - async def edit(self) -> TextChannel | None: - ... - - async def edit(self, *, reason=None, **options): - """|coro| - - Edits the channel. - - You must have the :attr:`~Permissions.manage_channels` permission to - use this. - - .. versionchanged:: 1.3 - The ``overwrites`` keyword-only parameter was added. - - .. versionchanged:: 1.4 - The ``type`` keyword-only parameter was added. - - .. versionchanged:: 2.0 - Edits are no longer in-place, the newly edited channel is returned instead. - - Parameters - ---------- - name: :class:`str` - The new channel name. - topic: :class:`str` - The new channel's topic. - position: :class:`int` - The new channel's position. - nsfw: :class:`bool` - To mark the channel as NSFW or not. - sync_permissions: :class:`bool` - Whether to sync permissions with the channel's new or pre-existing - category. Defaults to ``False``. - category: Optional[:class:`CategoryChannel`] - The new category for this channel. Can be ``None`` to remove the - category. - slowmode_delay: :class:`int` - Specifies the slowmode rate limit for user in this channel, in seconds. - A value of `0` disables slowmode. The maximum value possible is `21600`. - type: :class:`ChannelType` - Change the type of this text channel. Currently, only conversion between - :attr:`ChannelType.text` and :attr:`ChannelType.news` is supported. This - is only available to guilds that contain ``NEWS`` in :attr:`Guild.features`. - reason: Optional[:class:`str`] - The reason for editing this channel. Shows up on the audit log. - overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] - The overwrites to apply to channel permissions. Useful for creating secret channels. - 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``. - - Returns - ------- - Optional[:class:`.TextChannel`] - The newly edited text channel. If the edit was only positional - then ``None`` is returned instead. - - Raises - ------ - InvalidArgument - If position is less than 0 or greater than the number of channels, or if - the permission overwrite information is not in proper form. - Forbidden - You do not have permissions to edit the channel. - HTTPException - Editing the channel failed. - """ - - 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 edit(self, **options) -> _TextChannel: + """Edits the channel.""" + raise NotImplementedError @utils.copy_doc(discord.abc.GuildChannel.clone) async def clone( @@ -721,6 +733,10 @@ class TextChannel(discord.abc.Messageable, _TextChannel): Extra features of the channel. .. versionadded:: 2.0 + default_thread_slowmode_delay: Optional[:class:`int`] + The initial slowmode delay to set on newly created threads in this channel. + + .. versionadded:: 2.3 """ def __init__( @@ -739,7 +755,7 @@ async def _get_channel(self) -> "TextChannel": return self def is_news(self) -> bool: - """:class:`bool`: Checks if the channel is a news/announcements channel.""" + """Checks if the channel is a news/announcements channel.""" return self._type == ChannelType.news.value @property @@ -747,6 +763,103 @@ def news(self) -> bool: """Equivalent to :meth:`is_news`.""" return self.is_news() + @overload + async def edit( + self, + *, + reason: str | None = ..., + name: str = ..., + topic: str = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: CategoryChannel | None = ..., + slowmode_delay: int = ..., + default_auto_archive_duration: ThreadArchiveDuration = ..., + default_thread_slowmode_delay: int = ..., + type: ChannelType = ..., + overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + ) -> TextChannel | None: + ... + + @overload + async def edit(self) -> TextChannel | None: + ... + + async def edit(self, *, reason=None, **options): + """|coro| + + Edits the channel. + + You must have the :attr:`~Permissions.manage_channels` permission to + use this. + + .. versionchanged:: 1.3 + The ``overwrites`` keyword-only parameter was added. + + .. versionchanged:: 1.4 + The ``type`` keyword-only parameter was added. + + .. versionchanged:: 2.0 + Edits are no longer in-place, the newly edited channel is returned instead. + + Parameters + ---------- + name: :class:`str` + The new channel name. + topic: :class:`str` + The new channel's topic. + position: :class:`int` + The new channel's position. + nsfw: :class:`bool` + To mark the channel as NSFW or not. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`CategoryChannel`] + The new category for this channel. Can be ``None`` to remove the + category. + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of `0` disables slowmode. The maximum value possible is `21600`. + type: :class:`ChannelType` + Change the type of this text channel. Currently, only conversion between + :attr:`ChannelType.text` and :attr:`ChannelType.news` is supported. This + is only available to guilds that contain ``NEWS`` in :attr:`Guild.features`. + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] + The overwrites to apply to channel permissions. Useful for creating secret channels. + 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``. + default_thread_slowmode_delay: :class:`int` + The new default slowmode delay in seconds for threads created in this channel. + + .. versionadded:: 2.3 + + Returns + ------- + Optional[:class:`.TextChannel`] + The newly edited text channel. If the edit was only positional + then ``None`` is returned instead. + + Raises + ------ + InvalidArgument + If position is less than 0 or greater than the number of channels, or if + the permission overwrite information is not in proper form. + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + """ + + 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_thread( self, *, @@ -883,6 +996,18 @@ class ForumChannel(_TextChannel): Extra features of the channel. .. versionadded:: 2.0 + available_tags: List[:class:`ForumTag`] + The set of tags that can be used in a forum channel. + + .. versionadded:: 2.3 + default_sort_order: Optional[:class:`SortOrder`] + The default sort order type used to order posts in this channel. + + .. versionadded:: 2.3 + default_thread_slowmode_delay: Optional[:class:`int`] + The initial slowmode delay to set on newly created threads in this channel. + + .. versionadded:: 2.3 """ def __init__( @@ -892,12 +1017,133 @@ def __init__( def _update(self, guild: Guild, data: ForumChannelPayload) -> None: super()._update(guild, data) + self.available_tags: list[ForumTag] = [ + ForumTag.from_data(state=self._state, data=tag) + for tag in (data.get("available_tags") or []) + ] + self.default_sort_order: SortOrder | None = data.get("default_sort_order", None) @property def guidelines(self) -> str | None: - """Optional[:class:`str`]: The channel's guidelines. An alias of :attr:`topic`.""" + """The channel's guidelines. An alias of :attr:`topic`.""" return self.topic + @property + def requires_tag(self) -> bool: + """Whether a tag is required to be specified when creating a thread in this forum channel. + + Tags are specified in :attr:`applied_tags`. + + .. versionadded:: 2.3 + """ + return self.flags.require_tag + + def get_tag(self, id: int, /) -> ForumTag | None: + """Returns the :class:`ForumTag` from this forum channel with the + given ID, if any. + + .. versionadded:: 2.3 + """ + return utils.get(self.available_tags, id=id) + + @overload + async def edit( + self, + *, + reason: str | None = ..., + name: str = ..., + topic: str = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: CategoryChannel | None = ..., + slowmode_delay: int = ..., + default_auto_archive_duration: ThreadArchiveDuration = ..., + default_thread_slowmode_delay: int = ..., + default_sort_order: SortOrder = ..., + available_tags: list[ForumTag] = ..., + require_tag: bool = ..., + overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + ) -> "ForumChannel" | None: + ... + + @overload + async def edit(self) -> "ForumChannel" | None: + ... + + async def edit(self, *, reason=None, **options): + """|coro| + + Edits the channel. + + You must have the :attr:`~Permissions.manage_channels` permission to + use this. + + Parameters + ---------- + name: :class:`str` + The new channel name. + topic: :class:`str` + The new channel's topic. + position: :class:`int` + The new channel's position. + nsfw: :class:`bool` + To mark the channel as NSFW or not. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`CategoryChannel`] + The new category for this channel. Can be ``None`` to remove the + category. + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of `0` disables slowmode. The maximum value possible is `21600`. + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] + The overwrites to apply to channel permissions. Useful for creating secret channels. + 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``. + default_thread_slowmode_delay: :class:`int` + The new default slowmode delay in seconds for threads created in this channel. + + .. versionadded:: 2.3 + default_sort_order: Optional[:class:`SortOrder`] + The default sort order type to use to order posts in this channel. + + .. versionadded:: 2.3 + available_tags: List[:class:`ForumTag`] + The set of tags that can be used in this channel. Must be less than `20`. + + .. versionadded:: 2.3 + require_tag: :class:`bool` + Whether a tag should be required to be specified when creating a thread in this channel. + + .. versionadded:: 2.3 + + Returns + ------- + Optional[:class:`.ForumChannel`] + The newly edited forum channel. If the edit was only positional + then ``None`` is returned instead. + + Raises + ------ + InvalidArgument + If position is less than 0 or greater than the number of channels, or if + the permission overwrite information is not in proper form. + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + """ + + 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_thread( self, name: str, @@ -912,6 +1158,7 @@ async def create_thread( nonce=None, allowed_mentions=None, view=None, + applied_tags=None, auto_archive_duration: ThreadArchiveDuration = MISSING, slowmode_delay: int = MISSING, reason: str | None = None, @@ -955,6 +1202,8 @@ async def create_thread( are used instead. view: :class:`discord.ui.View` A Discord UI View to add to the message. + applied_tags: List[:class:`discord.ForumTag`] + A list of tags to apply to the new thread. auto_archive_duration: :class:`int` The duration in minutes before a thread is automatically archived for inactivity. If not provided, the channel's default auto archive duration is used. @@ -1019,6 +1268,9 @@ async def create_thread( else: components = None + if applied_tags is not None: + applied_tags = [str(tag.id) for tag in applied_tags] + if file is not None and files is not None: raise InvalidArgument("cannot pass both file and files parameter to send()") @@ -1078,6 +1330,7 @@ async def create_thread( auto_archive_duration=auto_archive_duration or self.default_auto_archive_duration, rate_limit_per_user=slowmode_delay or self.slowmode_delay, + applied_tags=applied_tags, reason=reason, ) ret = Thread(guild=self.guild, state=self._state, data=data) @@ -1156,7 +1409,7 @@ def _sorting_bucket(self) -> int: @property def members(self) -> list[Member]: - """List[:class:`Member`]: Returns all members that are currently inside this voice channel.""" + """Returns all members that are currently inside this voice channel.""" ret = [] for user_id, state in self.guild._voice_states.items(): if state.channel and state.channel.id == self.id: @@ -1281,7 +1534,7 @@ async def _get_channel(self): return self def is_nsfw(self) -> bool: - """:class:`bool`: Checks if the channel is NSFW.""" + """Checks if the channel is NSFW.""" return self.nsfw @property @@ -1536,7 +1789,7 @@ async def create_webhook( @property def type(self) -> ChannelType: - """:class:`ChannelType`: The channel's Discord type.""" + """The channel's Discord type.""" return ChannelType.voice @utils.copy_doc(discord.abc.GuildChannel.clone) @@ -1769,7 +2022,7 @@ def _update(self, guild: Guild, data: StageChannelPayload) -> None: @property def requesting_to_speak(self) -> list[Member]: - """List[:class:`Member`]: A list of members who are requesting to speak in the stage channel.""" + """A list of members who are requesting to speak in the stage channel.""" return [ member for member in self.members @@ -1778,7 +2031,7 @@ def requesting_to_speak(self) -> list[Member]: @property def speakers(self) -> list[Member]: - """List[:class:`Member`]: A list of members who have been permitted to speak in the stage channel. + """A list of members who have been permitted to speak in the stage channel. .. versionadded:: 2.0 """ @@ -1792,7 +2045,7 @@ def speakers(self) -> list[Member]: @property def listeners(self) -> list[Member]: - """List[:class:`Member`]: A list of members who are listening in the stage channel. + """A list of members who are listening in the stage channel. .. versionadded:: 2.0 """ @@ -1802,7 +2055,7 @@ def listeners(self) -> list[Member]: @property def moderators(self) -> list[Member]: - """List[:class:`Member`]: A list of members who are moderating the stage channel. + """A list of members who are moderating the stage channel. .. versionadded:: 2.0 """ @@ -1815,7 +2068,7 @@ def moderators(self) -> list[Member]: @property def type(self) -> ChannelType: - """:class:`ChannelType`: The channel's Discord type.""" + """The channel's Discord type.""" return ChannelType.stage_voice @utils.copy_doc(discord.abc.GuildChannel.clone) @@ -1826,7 +2079,7 @@ async def clone( @property def instance(self) -> StageInstance | None: - """Optional[:class:`StageInstance`]: The running stage instance of the stage channel. + """The running stage instance of the stage channel. .. versionadded:: 2.0 """ @@ -2061,7 +2314,10 @@ def __init__( self._update(guild, data) def __repr__(self) -> str: - return f"" + return ( + "" + ) def _update(self, guild: Guild, data: CategoryChannelPayload) -> None: # This data will always exist @@ -2082,11 +2338,11 @@ def _sorting_bucket(self) -> int: @property def type(self) -> ChannelType: - """:class:`ChannelType`: The channel's Discord type.""" + """The channel's Discord type.""" return ChannelType.category def is_nsfw(self) -> bool: - """:class:`bool`: Checks if the category is NSFW.""" + """Checks if the category is NSFW.""" return self.nsfw @utils.copy_doc(discord.abc.GuildChannel.clone) @@ -2166,7 +2422,7 @@ async def move(self, **kwargs): @property def channels(self) -> list[GuildChannelType]: - """List[:class:`abc.GuildChannel`]: Returns the channels that are under this category. + """Returns the channels that are under this category. These are sorted by the official Discord UI, which places voice channels below the text channels. """ @@ -2180,7 +2436,7 @@ def comparator(channel): @property def text_channels(self) -> list[TextChannel]: - """List[:class:`TextChannel`]: Returns the text channels that are under this category.""" + """Returns the text channels that are under this category.""" ret = [ c for c in self.guild.channels @@ -2191,7 +2447,7 @@ def text_channels(self) -> list[TextChannel]: @property def voice_channels(self) -> list[VoiceChannel]: - """List[:class:`VoiceChannel`]: Returns the voice channels that are under this category.""" + """Returns the voice channels that are under this category.""" ret = [ c for c in self.guild.channels @@ -2202,7 +2458,7 @@ def voice_channels(self) -> list[VoiceChannel]: @property def stage_channels(self) -> list[StageChannel]: - """List[:class:`StageChannel`]: Returns the stage channels that are under this category. + """Returns the stage channels that are under this category. .. versionadded:: 1.7 """ @@ -2216,7 +2472,7 @@ def stage_channels(self) -> list[StageChannel]: @property def forum_channels(self) -> list[ForumChannel]: - """List[:class:`ForumChannel`]: Returns the forum channels that are under this category. + """Returns the forum channels that are under this category. .. versionadded:: 2.0 """ @@ -2350,12 +2606,12 @@ def _from_message(cls: type[DMC], state: ConnectionState, channel_id: int) -> DM @property def type(self) -> ChannelType: - """:class:`ChannelType`: The channel's Discord type.""" + """The channel's Discord type.""" return ChannelType.private @property def jump_url(self) -> str: - """:class:`str`: Returns a URL that allows the client to jump to the channel. + """Returns a URL that allows the client to jump to the channel. .. versionadded:: 2.0 """ @@ -2363,7 +2619,7 @@ def jump_url(self) -> str: @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the direct message channel's creation time in UTC.""" + """Returns the direct message channel's creation time in UTC.""" return utils.snowflake_time(self.id) def permissions_for(self, obj: Any = None, /) -> Permissions: @@ -2509,24 +2765,24 @@ def __repr__(self) -> str: @property def type(self) -> ChannelType: - """:class:`ChannelType`: The channel's Discord type.""" + """The channel's Discord type.""" return ChannelType.group @property def icon(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the channel's icon asset if available.""" + """Returns the channel's icon asset if available.""" if self._icon is None: return None return Asset._from_icon(self._state, self.id, self._icon, path="channel") @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the channel's creation time in UTC.""" + """Returns the channel's creation time in UTC.""" return utils.snowflake_time(self.id) @property def jump_url(self) -> str: - """:class:`str`: Returns a URL that allows the client to jump to the channel. + """Returns a URL that allows the client to jump to the channel. .. versionadded:: 2.0 """ diff --git a/discord/client.py b/discord/client.py index e6fa165c..21abf9e7 100644 --- a/discord/client.py +++ b/discord/client.py @@ -37,6 +37,7 @@ from . import utils from .activity import ActivityTypes, BaseActivity, create_activity from .appinfo import AppInfo, PartialAppInfo +from .application_role_connection import ApplicationRoleConnectionMetadata from .backoff import ExponentialBackoff from .channel import PartialMessageable, _threaded_channel_factory from .emoji import Emoji @@ -273,7 +274,7 @@ def _handle_ready(self) -> None: @property def latency(self) -> float: - """:class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. + """Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. This could be referred to as the Discord WebSocket protocol latency. """ @@ -281,7 +282,7 @@ def latency(self) -> float: return float("nan") if not ws else ws.latency def is_ws_ratelimited(self) -> bool: - """:class:`bool`: Whether the WebSocket is currently rate limited. + """Whether the WebSocket is currently rate limited. This can be useful to know when deciding whether you should query members using HTTP or via the gateway. @@ -294,22 +295,22 @@ def is_ws_ratelimited(self) -> bool: @property def user(self) -> ClientUser | None: - """Optional[:class:`.ClientUser`]: Represents the connected client. ``None`` if not logged in.""" + """Represents the connected client. ``None`` if not logged in.""" return self._connection.user @property def guilds(self) -> list[Guild]: - """List[:class:`.Guild`]: The guilds that the connected client is a member of.""" + """The guilds that the connected client is a member of.""" return self._connection.guilds @property def emojis(self) -> list[Emoji]: - """List[:class:`.Emoji`]: The emojis that the connected client has.""" + """The emojis that the connected client has.""" return self._connection.emojis @property def stickers(self) -> list[GuildSticker]: - """List[:class:`.GuildSticker`]: The stickers that the connected client has. + """The stickers that the connected client has. .. versionadded:: 2.0 """ @@ -317,7 +318,7 @@ def stickers(self) -> list[GuildSticker]: @property def cached_messages(self) -> Sequence[Message]: - """Sequence[:class:`.Message`]: Read-only list of messages the connected client has cached. + """Read-only list of messages the connected client has cached. .. versionadded:: 1.1 """ @@ -325,7 +326,7 @@ def cached_messages(self) -> Sequence[Message]: @property def private_channels(self) -> list[PrivateChannel]: - """List[:class:`.abc.PrivateChannel`]: The private channels that the connected client is participating on. + """The private channels that the connected client is participating on. .. note:: @@ -336,7 +337,7 @@ def private_channels(self) -> list[PrivateChannel]: @property def voice_clients(self) -> list[VoiceProtocol]: - """List[:class:`.VoiceProtocol`]: Represents a list of voice connections. + """Represents a list of voice connections. These are usually :class:`.VoiceClient` instances. """ @@ -344,7 +345,7 @@ def voice_clients(self) -> list[VoiceProtocol]: @property def application_id(self) -> int | None: - """Optional[:class:`int`]: The client's application ID. + """The client's application ID. If this is not passed via ``__init__`` then this is retrieved through the gateway when an event contains the data. Usually @@ -356,14 +357,14 @@ def application_id(self) -> int | None: @property def application_flags(self) -> ApplicationFlags: - """:class:`~discord.ApplicationFlags`: The client's application flags. + """The client's application flags. .. versionadded:: 2.0 """ return self._connection.application_flags # type: ignore def is_ready(self) -> bool: - """:class:`bool`: Specifies if the client's internal cache is ready for use.""" + """Specifies if the client's internal cache is ready for use.""" return self._ready.is_set() async def _run_event( @@ -568,7 +569,6 @@ async def connect(self, *, reconnect: bool = True) -> None: aiohttp.ClientError, asyncio.TimeoutError, ) as exc: - self.dispatch("disconnect") if not reconnect: await self.close() @@ -722,13 +722,16 @@ def stop_loop_on_completion(f): # properties def is_closed(self) -> bool: - """:class:`bool`: Indicates if the WebSocket connection is closed.""" + """Indicates if the WebSocket connection is closed.""" return self._closed @property def activity(self) -> ActivityTypes | None: - """Optional[:class:`.BaseActivity`]: The activity being used upon - logging in. + """The activity being used upon logging in. + + Returns + ------- + Optional[:class:`.BaseActivity`] """ return create_activity(self._connection._activity) @@ -743,9 +746,8 @@ def activity(self, value: ActivityTypes | None) -> None: raise TypeError("activity must derive from BaseActivity.") @property - def status(self): - """:class:`.Status`: - The status being used upon logging on to Discord. + def status(self) -> Status: + """The status being used upon logging on to Discord. .. versionadded: 2.0 """ @@ -754,7 +756,7 @@ def status(self): return Status.online @status.setter - def status(self, value): + def status(self, value: Status) -> None: if value is Status.offline: self._connection._status = "invisible" elif isinstance(value, Status): @@ -764,7 +766,7 @@ def status(self, value): @property def allowed_mentions(self) -> AllowedMentions | None: - """Optional[:class:`~discord.AllowedMentions`]: The allowed mention configuration. + """The allowed mention configuration. .. versionadded:: 1.4 """ @@ -781,7 +783,7 @@ def allowed_mentions(self, value: AllowedMentions | None) -> None: @property def intents(self) -> Intents: - """:class:`~discord.Intents`: The intents configured for this connection. + """The intents configured for this connection. .. versionadded:: 1.5 """ @@ -791,7 +793,7 @@ def intents(self) -> Intents: @property def users(self) -> list[User]: - """List[:class:`~discord.User`]: Returns a list of all the users the bot can see.""" + """Returns a list of all the users the bot can see.""" return list(self._connection._users.values()) async def fetch_application(self, application_id: int, /) -> PartialAppInfo: @@ -1768,15 +1770,60 @@ def add_view(self, view: View, *, message_id: int | None = None) -> None: if not view.is_persistent(): raise ValueError( - "View is not persistent. Items need to have a custom_id set and View must have no timeout" + "View is not persistent. Items need to have a custom_id set and View" + " must have no timeout" ) self._connection.store_view(view, message_id) @property def persistent_views(self) -> Sequence[View]: - """Sequence[:class:`.View`]: A sequence of persistent views added to the client. + """A sequence of persistent views added to the client. .. versionadded:: 2.0 """ return self._connection.persistent_views + + async def fetch_role_connection_metadata_records( + self, + ) -> list[ApplicationRoleConnectionMetadata]: + """|coro| + + Fetches the bot's role connection metadata records. + + .. versionadded:: 2.4 + + Returns + ------- + List[:class:`.ApplicationRoleConnectionMetadata`] + The bot's role connection metadata records. + """ + data = await self._connection.http.get_application_role_connection_metadata_records( + self.application_id + ) + return [ApplicationRoleConnectionMetadata.from_dict(r) for r in data] + + async def update_role_connection_metadata_records( + self, *role_connection_metadata + ) -> list[ApplicationRoleConnectionMetadata]: + """|coro| + + Updates the bot's role connection metadata records. + + .. versionadded:: 2.4 + + Parameters + ---------- + *role_connection_metadata: :class:`ApplicationRoleConnectionMetadata` + The new metadata records to send to Discord. + + Returns + ------- + List[:class:`.ApplicationRoleConnectionMetadata`] + The updated role connection metadata records. + """ + payload = [r.to_dict() for r in role_connection_metadata] + data = await self._connection.http.update_application_role_connection_metadata_records( + self.application_id, payload + ) + return [ApplicationRoleConnectionMetadata.from_dict(r) for r in data] diff --git a/discord/cog.py b/discord/cog.py index a634e369..9e3d8733 100644 --- a/discord/cog.py +++ b/discord/cog.py @@ -143,7 +143,10 @@ def __new__(cls: type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta: commands = {} listeners = {} - no_bot_cog = "Commands or listeners must not start with cog_ or bot_ (in method {0.__name__}.{1})" + no_bot_cog = ( + "Commands or listeners must not start with cog_ or bot_ (in method" + " {0.__name__}.{1})" + ) new_cls = super().__new__(cls, name, bases, attrs, **kwargs) @@ -177,7 +180,8 @@ def __new__(cls: type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta: if isinstance(value, _filter): if is_static_method: raise TypeError( - f"Command in method {base}.{elem!r} must not be staticmethod." + f"Command in method {base}.{elem!r} must not be" + " staticmethod." ) if elem.startswith(("cog_", "bot_")): raise TypeError(no_bot_cog.format(base, elem)) @@ -187,13 +191,15 @@ def __new__(cls: type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta: if hasattr(value, "add_to") and not getattr(value, "parent", None): if is_static_method: raise TypeError( - f"Command in method {base}.{elem!r} must not be staticmethod." + f"Command in method {base}.{elem!r} must not be" + " staticmethod." ) if elem.startswith(("cog_", "bot_")): raise TypeError(no_bot_cog.format(base, elem)) commands[f"ext_{elem}"] = value.ext_variant commands[f"app_{elem}"] = value.slash_variant + commands[elem] = value for cmd in getattr(value, "subcommands", []): commands[ f"ext_{cmd.ext_variant.qualified_name}" @@ -224,9 +230,13 @@ def __new__(cls: type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta: # Either update the command with the cog provided defaults or copy it. # r.e type ignore, type-checker complains about overriding a ClassVar - new_cls.__cog_commands__ = tuple(c._update_copy(cmd_attrs) for c in new_cls.__cog_commands__) # type: ignore + new_cls.__cog_commands__ = tuple(c._update_copy(cmd_attrs) if not hasattr(c, "add_to") else c for c in new_cls.__cog_commands__) # type: ignore - name_filter = lambda c: "app" if isinstance(c, ApplicationCommand) else "ext" + name_filter = lambda c: ( + "app" + if isinstance(c, ApplicationCommand) + else ("bridge" if not hasattr(c, "add_to") else "ext") + ) lookup = { f"{name_filter(cmd)}_{cmd.qualified_name}": cmd @@ -242,7 +252,9 @@ def __new__(cls: type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta: ): command.guild_ids = new_cls.__cog_guild_ids__ - if not isinstance(command, SlashCommandGroup): + if not isinstance(command, SlashCommandGroup) and not hasattr( + command, "add_to" + ): # ignore bridge commands cmd = getattr(new_cls, command.callback.__name__, None) if hasattr(cmd, "add_to"): @@ -321,12 +333,12 @@ def get_commands(self) -> list[ApplicationCommand]: @property def qualified_name(self) -> str: - """:class:`str`: Returns the cog's specified name, not the class name.""" + """Returns the cog's specified name, not the class name.""" return self.__cog_name__ @property def description(self) -> str: - """:class:`str`: Returns the cog's description, typically the cleaned docstring.""" + """Returns the cog's description, typically the cleaned docstring.""" return self.__cog_description__ @description.setter @@ -386,7 +398,8 @@ def listener(cls, name: str = MISSING) -> Callable[[FuncT], FuncT]: if name is not MISSING and not isinstance(name, str): raise TypeError( - f"Cog.listener expected str but received {name.__class__.__name__!r} instead." + "Cog.listener expected str but received" + f" {name.__class__.__name__!r} instead." ) def decorator(func: FuncT) -> FuncT: @@ -410,7 +423,7 @@ def decorator(func: FuncT) -> FuncT: return decorator def has_error_handler(self) -> bool: - """:class:`bool`: Checks whether the cog has an error handler. + """Checks whether the cog has an error handler. .. versionadded:: 1.7 """ @@ -528,6 +541,10 @@ def _inject(self: CogT, bot) -> CogT: # we've added so far for some form of atomic loading. for index, command in enumerate(self.__cog_commands__): + if hasattr(command, "add_to"): + bot.bridge_commands.append(command) + continue + command._set_cog(self) if isinstance(command, ApplicationCommand): @@ -692,7 +709,7 @@ def remove_cog(self, name: str) -> Cog | None: @property def cogs(self) -> Mapping[str, Cog]: - """Mapping[:class:`str`, :class:`Cog`]: A read-only mapping of cog name to cog.""" + """A read-only mapping of cog name to cog.""" return types.MappingProxyType(self.__cogs) # extensions @@ -1002,8 +1019,10 @@ def load_extensions( loaded = self.load_extension( ext_path, package=package, recursive=recursive, store=store ) - loaded_extensions.update(loaded) if store else loaded_extensions.extend( - loaded + ( + loaded_extensions.update(loaded) + if store + else loaded_extensions.extend(loaded) ) return loaded_extensions @@ -1114,5 +1133,5 @@ def reload_extension(self, name: str, *, package: str | None = None) -> None: @property def extensions(self) -> Mapping[str, types.ModuleType]: - """Mapping[:class:`str`, :class:`py:types.ModuleType`]: A read-only mapping of extension name to extension.""" + """A read-only mapping of extension name to extension.""" return types.MappingProxyType(self.__extensions) diff --git a/discord/colour.py b/discord/colour.py index eb4f139a..c2023518 100644 --- a/discord/colour.py +++ b/discord/colour.py @@ -22,10 +22,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations import colorsys import random -from typing import Any, Optional, Tuple, Type, TypeVar, Union +from typing import Any, TypeVar __all__ = ( "Colour", @@ -102,44 +103,44 @@ def __hash__(self) -> int: @property def r(self) -> int: - """:class:`int`: Returns the red component of the colour.""" + """Returns the red component of the colour.""" return self._get_byte(2) @property def g(self) -> int: - """:class:`int`: Returns the green component of the colour.""" + """Returns the green component of the colour.""" return self._get_byte(1) @property def b(self) -> int: - """:class:`int`: Returns the blue component of the colour.""" + """Returns the blue component of the colour.""" return self._get_byte(0) - def to_rgb(self) -> Tuple[int, int, int]: - """Tuple[:class:`int`, :class:`int`, :class:`int`]: Returns an (r, g, b) tuple representing the colour.""" + def to_rgb(self) -> tuple[int, int, int]: + """Returns an (r, g, b) tuple representing the colour.""" return self.r, self.g, self.b @classmethod - def from_rgb(cls: Type[CT], r: int, g: int, b: int) -> CT: + def from_rgb(cls: type[CT], r: int, g: int, b: int) -> CT: """Constructs a :class:`Colour` from an RGB tuple.""" return cls((r << 16) + (g << 8) + b) @classmethod - def from_hsv(cls: Type[CT], h: float, s: float, v: float) -> CT: + def from_hsv(cls: type[CT], h: float, s: float, v: float) -> CT: """Constructs a :class:`Colour` from an HSV tuple.""" rgb = colorsys.hsv_to_rgb(h, s, v) return cls.from_rgb(*(int(x * 255) for x in rgb)) @classmethod - def default(cls: Type[CT]) -> CT: + def default(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0``.""" return cls(0) @classmethod def random( - cls: Type[CT], + cls: type[CT], *, - seed: Optional[Union[int, str, float, bytes, bytearray]] = None, + seed: int | str | float | bytes | bytearray | None = None, ) -> CT: """A factory method that returns a :class:`Colour` with a random hue. @@ -161,17 +162,17 @@ def random( return cls.from_hsv(rand.random(), 1, 1) @classmethod - def teal(cls: Type[CT]) -> CT: + def teal(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``.""" return cls(0x1ABC9C) @classmethod - def dark_teal(cls: Type[CT]) -> CT: + def dark_teal(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x11806a``.""" return cls(0x11806A) @classmethod - def brand_green(cls: Type[CT]) -> CT: + def brand_green(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x57F287``. .. versionadded:: 2.0 @@ -179,67 +180,67 @@ def brand_green(cls: Type[CT]) -> CT: return cls(0x57F287) @classmethod - def green(cls: Type[CT]) -> CT: + def green(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``.""" return cls(0x2ECC71) @classmethod - def dark_green(cls: Type[CT]) -> CT: + def dark_green(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``.""" return cls(0x1F8B4C) @classmethod - def blue(cls: Type[CT]) -> CT: + def blue(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x3498db``.""" return cls(0x3498DB) @classmethod - def dark_blue(cls: Type[CT]) -> CT: + def dark_blue(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x206694``.""" return cls(0x206694) @classmethod - def purple(cls: Type[CT]) -> CT: + def purple(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``.""" return cls(0x9B59B6) @classmethod - def dark_purple(cls: Type[CT]) -> CT: + def dark_purple(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x71368a``.""" return cls(0x71368A) @classmethod - def magenta(cls: Type[CT]) -> CT: + def magenta(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xe91e63``.""" return cls(0xE91E63) @classmethod - def dark_magenta(cls: Type[CT]) -> CT: + def dark_magenta(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xad1457``.""" return cls(0xAD1457) @classmethod - def gold(cls: Type[CT]) -> CT: + def gold(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``.""" return cls(0xF1C40F) @classmethod - def dark_gold(cls: Type[CT]) -> CT: + def dark_gold(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``.""" return cls(0xC27C0E) @classmethod - def orange(cls: Type[CT]) -> CT: + def orange(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xe67e22``.""" return cls(0xE67E22) @classmethod - def dark_orange(cls: Type[CT]) -> CT: + def dark_orange(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xa84300``.""" return cls(0xA84300) @classmethod - def brand_red(cls: Type[CT]) -> CT: + def brand_red(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xED4245``. .. versionadded:: 2.0 @@ -247,60 +248,60 @@ def brand_red(cls: Type[CT]) -> CT: return cls(0xED4245) @classmethod - def red(cls: Type[CT]) -> CT: + def red(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``.""" return cls(0xE74C3C) @classmethod - def dark_red(cls: Type[CT]) -> CT: + def dark_red(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x992d22``.""" return cls(0x992D22) @classmethod - def lighter_grey(cls: Type[CT]) -> CT: + def lighter_grey(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``.""" return cls(0x95A5A6) lighter_gray = lighter_grey @classmethod - def dark_grey(cls: Type[CT]) -> CT: + def dark_grey(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x607d8b``.""" return cls(0x607D8B) dark_gray = dark_grey @classmethod - def light_grey(cls: Type[CT]) -> CT: + def light_grey(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x979c9f``.""" return cls(0x979C9F) light_gray = light_grey @classmethod - def darker_grey(cls: Type[CT]) -> CT: + def darker_grey(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x546e7a``.""" return cls(0x546E7A) darker_gray = darker_grey @classmethod - def og_blurple(cls: Type[CT]) -> CT: + def og_blurple(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x7289da``.""" return cls(0x7289DA) @classmethod - def blurple(cls: Type[CT]) -> CT: + def blurple(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x5865F2``.""" return cls(0x5865F2) @classmethod - def greyple(cls: Type[CT]) -> CT: + def greyple(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x99aab5``.""" return cls(0x99AAB5) @classmethod - def dark_theme(cls: Type[CT]) -> CT: + def dark_theme(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0x36393F``. This will appear transparent on Discord's dark theme. @@ -309,7 +310,7 @@ def dark_theme(cls: Type[CT]) -> CT: return cls(0x36393F) @classmethod - def fuchsia(cls: Type[CT]) -> CT: + def fuchsia(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xEB459E``. .. versionadded:: 2.0 @@ -317,7 +318,7 @@ def fuchsia(cls: Type[CT]) -> CT: return cls(0xEB459E) @classmethod - def yellow(cls: Type[CT]) -> CT: + def yellow(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xFEE75C``. .. versionadded:: 2.0 @@ -325,7 +326,7 @@ def yellow(cls: Type[CT]) -> CT: return cls(0xFEE75C) @classmethod - def nitro_pink(cls: Type[CT]) -> CT: + def nitro_pink(cls: type[CT]) -> CT: """A factory method that returns a :class:`Colour` with a value of ``0xf47fff``. .. versionadded:: 2.0 @@ -333,12 +334,12 @@ def nitro_pink(cls: Type[CT]) -> CT: return cls(0xF47FFF) @classmethod - def embed_background(cls: Type[CT], theme: str = "dark") -> CT: + def embed_background(cls: type[CT], theme: str = "dark") -> CT: """A factory method that returns a :class:`Color` corresponding to the embed colors on discord clients, with a value of: - - ``0x2F3136`` (dark) - - ``0xf2f3f5`` (light) + - ``0x2B2D31`` (dark) + - ``0xEEEFF1`` (light) - ``0x000000`` (amoled). .. versionadded:: 2.0 @@ -349,8 +350,8 @@ def embed_background(cls: Type[CT], theme: str = "dark") -> CT: The theme color to apply, must be one of "dark", "light", or "amoled". """ themes_cls = { - "dark": 0x2F3136, - "light": 0xF2F3F5, + "dark": 0x2B2D31, + "light": 0xEEEFF1, "amoled": 0x000000, } diff --git a/discord/commands/context.py b/discord/commands/context.py index 212f7964..f20db34e 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -144,35 +144,35 @@ def channel(self) -> InteractionChannel | None: @cached_property def channel_id(self) -> int | None: - """:class:`int`: Returns the ID of the channel associated with this context's command. + """Returns the ID of the channel associated with this context's command. Shorthand for :attr:`.Interaction.channel_id`. """ return self.interaction.channel_id @cached_property def guild(self) -> Guild | None: - """Optional[:class:`.Guild`]: Returns the guild associated with this context's command. + """Returns the guild associated with this context's command. Shorthand for :attr:`.Interaction.guild`. """ return self.interaction.guild @cached_property def guild_id(self) -> int | None: - """:class:`int`: Returns the ID of the guild associated with this context's command. + """Returns the ID of the guild associated with this context's command. Shorthand for :attr:`.Interaction.guild_id`. """ return self.interaction.guild_id @cached_property def locale(self) -> str | None: - """:class:`str`: Returns the locale of the guild associated with this context's command. + """Returns the locale of the guild associated with this context's command. Shorthand for :attr:`.Interaction.locale`. """ return self.interaction.locale @cached_property def guild_locale(self) -> str | None: - """:class:`str`: Returns the locale of the guild associated with this context's command. + """Returns the locale of the guild associated with this context's command. Shorthand for :attr:`.Interaction.guild_locale`. """ return self.interaction.guild_locale @@ -195,23 +195,23 @@ def me(self) -> Member | ClientUser | None: @cached_property def message(self) -> Message | None: - """Optional[:class:`.Message`]: Returns the message sent with this context's command. + """Returns the message sent with this context's command. Shorthand for :attr:`.Interaction.message`, if applicable. """ return self.interaction.message @cached_property - def user(self) -> Member | User | None: - """Union[:class:`.Member`, :class:`.User`]: Returns the user that sent this context's command. + def user(self) -> Member | User: + """Returns the user that sent this context's command. Shorthand for :attr:`.Interaction.user`. """ - return self.interaction.user + return self.interaction.user # type: ignore # command user will never be None - author: Member | User | None = user + author = user @property def voice_client(self) -> VoiceProtocol | None: - """Optional[:class:`.VoiceProtocol`]: Returns the voice client associated with this context's command. + """Returns the voice client associated with this context's command. Shorthand for :attr:`Interaction.guild.voice_client<~discord.Guild.voice_client>`, if applicable. """ if self.interaction.guild is None: @@ -221,7 +221,7 @@ def voice_client(self) -> VoiceProtocol | None: @cached_property def response(self) -> InteractionResponse: - """:class:`.InteractionResponse`: Returns the response object associated with this context's command. + """Returns the response object associated with this context's command. Shorthand for :attr:`.Interaction.response`. """ return self.interaction.response @@ -294,7 +294,8 @@ def send_response(self) -> Callable[..., Awaitable[Interaction]]: return self.interaction.response.send_message else: raise RuntimeError( - f"Interaction was already issued a response. Try using {type(self).__name__}.send_followup() instead." + "Interaction was already issued a response. Try using" + f" {type(self).__name__}.send_followup() instead." ) @property @@ -304,7 +305,8 @@ def send_followup(self) -> Callable[..., Awaitable[WebhookMessage]]: return self.followup.send else: raise RuntimeError( - f"Interaction was not yet issued a response. Try using {type(self).__name__}.respond() first." + "Interaction was not yet issued a response. Try using" + f" {type(self).__name__}.respond() first." ) @property @@ -314,7 +316,7 @@ def defer(self) -> Callable[..., Awaitable[None]]: @property def followup(self) -> Webhook: - """:class:`Webhook`: Returns the followup webhook for followup interactions.""" + """Returns the followup webhook for followup interactions.""" return self.interaction.followup async def delete(self, *, delay: float | None = None) -> None: @@ -348,7 +350,7 @@ def edit(self) -> Callable[..., Awaitable[InteractionMessage]]: @property def cog(self) -> Cog | None: - """Optional[:class:`.Cog`]: Returns the cog associated with this context's command. + """Returns the cog associated with this context's command. ``None`` if it does not exist. """ if self.command is None: @@ -393,7 +395,7 @@ def __init__(self, bot: Bot, interaction: Interaction): @property def cog(self) -> Cog | None: - """Optional[:class:`.Cog`]: Returns the cog associated with this context's command. + """Returns the cog associated with this context's command. ``None`` if it does not exist. """ if self.command is None: diff --git a/discord/commands/core.py b/discord/commands/core.py index 7862333c..d4f7c0ff 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -222,6 +222,7 @@ def __init__(self, func: Callable, **kwargs) -> None: self.guild_only: bool | None = getattr( func, "__guild_only__", kwargs.get("guild_only", None) ) + self.nsfw: bool | None = getattr(func, "__nsfw__", kwargs.get("nsfw", None)) def __repr__(self) -> str: return f"" @@ -374,7 +375,6 @@ async def invoke(self, ctx: ApplicationContext) -> None: await injected(ctx) async def can_run(self, ctx: ApplicationContext) -> bool: - if not await ctx.bot.can_run(ctx): raise CheckFailure( f"The global check functions for command {self.name} failed." @@ -450,7 +450,7 @@ def error(self, coro): return coro def has_error_handler(self) -> bool: - """:class:`bool`: Checks whether the command has an error handler registered.""" + """Checks whether the command has an error handler registered.""" return hasattr(self, "on_error") def before_invoke(self, coro): @@ -553,7 +553,7 @@ def cooldown(self): @property def full_parent_name(self) -> str: - """:class:`str`: Retrieves the fully qualified parent command name. + """Retrieves the fully qualified parent command name. This the base command name required to execute it. For example, in ``/one two three`` the parent name would be ``one two``. @@ -568,7 +568,7 @@ def full_parent_name(self) -> str: @property def qualified_name(self) -> str: - """:class:`str`: Retrieves the fully qualified command name. + """Retrieves the fully qualified command name. This is the full parent name with the command name as well. For example, in ``/one two three`` the qualified name would be @@ -584,7 +584,7 @@ def qualified_name(self) -> str: @property def qualified_id(self) -> int: - """:class:`int`: Retrieves the fully qualified command ID. + """Retrieves the fully qualified command ID. This is the root parent ID. For example, in ``/one two three`` the qualified ID would return ``one.id``. @@ -630,6 +630,9 @@ class SlashCommand(ApplicationCommand): Returns a string that allows you to mention the slash command. guild_only: :class:`bool` Whether the command should only be usable inside a guild. + nsfw: :class:`bool` + Whether the command should be restricted to 18+ channels and users. + Apps intending to be listed in the App Directory cannot have NSFW commands. default_member_permissions: :class:`~discord.Permissions` The default permissions a member needs to be able to run the command. cog: Optional[:class:`Cog`] @@ -850,6 +853,9 @@ def to_dict(self) -> dict: if self.guild_only is not None: as_dict["dm_permission"] = not self.guild_only + if self.nsfw is not None: + as_dict["nsfw"] = self.nsfw + if self.default_member_permissions is not None: as_dict[ "default_member_permissions" @@ -904,8 +910,10 @@ async def _invoke(self, ctx: ApplicationContext) -> None: ): arg = ctx.guild.get_channel_or_thread(int(arg)) _data["_invoke_flag"] = True - arg._update(_data) if isinstance(arg, Thread) else arg._update( - ctx.guild, _data + ( + arg._update(_data) + if isinstance(arg, Thread) + else arg._update(ctx.guild, _data) ) else: obj_type = None @@ -1061,6 +1069,9 @@ class SlashCommandGroup(ApplicationCommand): isn't one. guild_only: :class:`bool` Whether the command should only be usable inside a guild. + nsfw: :class:`bool` + Whether the command should be restricted to 18+ channels and users. + Apps intending to be listed in the App Directory cannot have NSFW commands. default_member_permissions: :class:`~discord.Permissions` The default permissions a member needs to be able to run the command. checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] @@ -1133,6 +1144,7 @@ def __init__( "default_member_permissions", None ) self.guild_only: bool | None = kwargs.get("guild_only", None) + self.nsfw: bool | None = kwargs.get("nsfw", None) self.name_localizations: dict[str, str] | None = kwargs.get( "name_localizations", None @@ -1162,6 +1174,9 @@ def to_dict(self) -> dict: if self.guild_only is not None: as_dict["dm_permission"] = not self.guild_only + if self.nsfw is not None: + as_dict["nsfw"] = self.nsfw + if self.default_member_permissions is not None: as_dict[ "default_member_permissions" @@ -1209,6 +1224,9 @@ def create_subgroup( This will be a global command if ``None`` is passed. guild_only: :class:`bool` Whether the command should only be usable inside a guild. + nsfw: :class:`bool` + Whether the command should be restricted to 18+ channels and users. + Apps intending to be listed in the App Directory cannot have NSFW commands. default_member_permissions: :class:`~discord.Permissions` The default permissions a member needs to be able to run the command. checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] @@ -1299,13 +1317,13 @@ async def invoke_autocomplete_callback(self, ctx: AutocompleteContext) -> None: ctx.interaction.data = option await command.invoke_autocomplete_callback(ctx) - def walk_commands(self) -> Generator[SlashCommand, None, None]: - """An iterator that recursively walks through all slash commands in this group. + def walk_commands(self) -> Generator[SlashCommand | SlashCommandGroup, None, None]: + """An iterator that recursively walks through all slash commands and groups in this group. Yields ------ - :class:`.SlashCommand` - A slash command from the group. + :class:`.SlashCommand` | :class:`.SlashCommandGroup` + A nested slash command or slash command group from the group. """ for command in self.subcommands: if isinstance(command, SlashCommandGroup): @@ -1378,6 +1396,9 @@ class ContextMenuCommand(ApplicationCommand): The ids of the guilds where this command will be registered. guild_only: :class:`bool` Whether the command should only be usable inside a guild. + nsfw: :class:`bool` + Whether the command should be restricted to 18+ channels and users. + Apps intending to be listed in the App Directory cannot have NSFW commands. default_member_permissions: :class:`~discord.Permissions` The default permissions a member needs to be able to run the command. cog: Optional[:class:`Cog`] @@ -1477,6 +1498,9 @@ def to_dict(self) -> dict[str, str | int]: if self.guild_only is not None: as_dict["dm_permission"] = not self.guild_only + if self.nsfw is not None: + as_dict["nsfw"] = self.nsfw + if self.default_member_permissions is not None: as_dict[ "default_member_permissions" @@ -1822,7 +1846,8 @@ def validate_chat_input_name(name: Any, locale: str | None = None): # Must meet the regex ^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$ if locale is not None and locale not in valid_locales: raise ValidationError( - f"Locale '{locale}' is not a valid locale, see {docs}/reference#locales for list of supported locales." + f"Locale '{locale}' is not a valid locale, see {docs}/reference#locales for" + " list of supported locales." ) error = None if not isinstance(name, str): @@ -1831,8 +1856,10 @@ def validate_chat_input_name(name: Any, locale: str | None = None): ) elif not re.match(r"^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$", name): error = ValidationError( - r"Command names and options must follow the regex \"^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$\". " - f"For more information, see {docs}/interactions/application-commands#application-command-object-" + r"Command names and options must follow the regex" + r" \"^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$\". " + "For more information, see" + f" {docs}/interactions/application-commands#application-command-object-" f'application-command-naming. Received "{name}"' ) elif ( @@ -1851,16 +1878,19 @@ def validate_chat_input_name(name: Any, locale: str | None = None): def validate_chat_input_description(description: Any, locale: str | None = None): if locale is not None and locale not in valid_locales: raise ValidationError( - f"Locale '{locale}' is not a valid locale, see {docs}/reference#locales for list of supported locales." + f"Locale '{locale}' is not a valid locale, see {docs}/reference#locales for" + " list of supported locales." ) error = None if not isinstance(description, str): error = TypeError( - f'Command and option description must be of type str. Received "{description}"' + "Command and option description must be of type str. Received" + f' "{description}"' ) elif not 1 <= len(description) <= 100: error = ValidationError( - f'Command and option description must be 1-100 characters long. Received "{description}"' + "Command and option description must be 1-100 characters long. Received" + f' "{description}"' ) if error: diff --git a/discord/commands/options.py b/discord/commands/options.py index c5c013a3..d6c04faf 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -29,7 +29,15 @@ from typing import TYPE_CHECKING, Literal, Optional, Type, Union from ..abc import GuildChannel, Mentionable -from ..channel import CategoryChannel, StageChannel, TextChannel, Thread, VoiceChannel +from ..channel import ( + CategoryChannel, + DMChannel, + ForumChannel, + StageChannel, + TextChannel, + Thread, + VoiceChannel, +) from ..enums import ChannelType from ..enums import Enum as DiscordEnum from ..enums import SlashCommandOptionType @@ -73,6 +81,8 @@ StageChannel: ChannelType.stage_voice, CategoryChannel: ChannelType.category, Thread: ChannelType.public_thread, + ForumChannel: ChannelType.forum, + DMChannel: ChannelType.private, } @@ -138,6 +148,10 @@ class Option: .. note:: Does not validate the input value against the autocomplete results. + channel_types: list[:class:`discord.ChannelType`] | None + A list of channel types that can be selected in this option. + Only applies to Options with an :attr:`input_type` of :class:`discord.SlashCommandOptionType.channel`. + If this argument is used, :attr:`input_type` will be ignored. name_localizations: Optional[Dict[:class:`str`, :class:`str`]] The name localizations for this option. The values of this should be ``"locale": "name"``. See `here `_ for a list of valid locales. @@ -178,7 +192,8 @@ def __init__( enum_choices = [] input_type_is_class = isinstance(input_type, type) if input_type_is_class and issubclass(input_type, (Enum, DiscordEnum)): - description = inspect.getdoc(input_type) + if description is None: + description = inspect.getdoc(input_type) enum_choices = [OptionChoice(e.name, e.value) for e in input_type] value_class = enum_choices[0].value.__class__ if all(isinstance(elem.value, value_class) for elem in enum_choices): @@ -223,18 +238,19 @@ def __init__( self._raw_type = input_type.__args__ # type: ignore # Union.__args__ else: self._raw_type = (input_type,) - self.channel_types = [ - CHANNEL_TYPE_MAP[t] - for t in self._raw_type - if t is not GuildChannel - ] + if not self.channel_types: + self.channel_types = [ + CHANNEL_TYPE_MAP[t] + for t in self._raw_type + if t is not GuildChannel + ] self.required: bool = ( kwargs.pop("required", True) if "default" not in kwargs else False ) self.default = kwargs.pop("default", None) self.choices: list[OptionChoice] = enum_choices or [ o if isinstance(o, OptionChoice) else OptionChoice(o) - for o in kwargs.pop("choices", list()) + for o in kwargs.pop("choices", []) ] if self.input_type == SlashCommandOptionType.integer: @@ -277,11 +293,13 @@ def __init__( if self.min_value is not None and not isinstance(self.min_value, minmax_types): raise TypeError( - f'Expected {minmax_typehint} for min_value, got "{type(self.min_value).__name__}"' + f"Expected {minmax_typehint} for min_value, got" + f' "{type(self.min_value).__name__}"' ) if self.max_value is not None and not isinstance(self.max_value, minmax_types): raise TypeError( - f'Expected {minmax_typehint} for max_value, got "{type(self.max_value).__name__}"' + f"Expected {minmax_typehint} for max_value, got" + f' "{type(self.max_value).__name__}"' ) if self.min_length is not None: diff --git a/discord/commands/permissions.py b/discord/commands/permissions.py index b6c1cc0a..df951ae0 100644 --- a/discord/commands/permissions.py +++ b/discord/commands/permissions.py @@ -28,10 +28,7 @@ from ..permissions import Permissions from .core import ApplicationCommand -__all__ = ( - "default_permissions", - "guild_only", -) +__all__ = ("default_permissions", "guild_only", "is_nsfw") def default_permissions(**perms: bool) -> Callable: @@ -108,3 +105,34 @@ def inner(command: Callable): return command return inner + + +def is_nsfw() -> Callable: + """A decorator that limits the usage of a slash command to 18+ channels and users. + In guilds, the command will only be able to be used in channels marked as NSFW. + In DMs, users must have opted into age-restricted commands via privacy settings. + + Note that apps intending to be listed in the App Directory cannot have NSFW commands. + + Example + ------- + + .. code-block:: python3 + + from discord import is_nsfw + + @bot.slash_command() + @is_nsfw() + async def test(ctx): + await ctx.respond("This command is age restricted.") + """ + + def inner(command: Callable): + if isinstance(command, ApplicationCommand): + command.nsfw = True + else: + command.__nsfw__ = True + + return command + + return inner diff --git a/discord/components.py b/discord/components.py index 5934b516..eb360f72 100644 --- a/discord/components.py +++ b/discord/components.py @@ -27,7 +27,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar -from .enums import ButtonStyle, ComponentType, InputTextStyle, try_enum +from .enums import ButtonStyle, ChannelType, ComponentType, InputTextStyle, try_enum from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots @@ -40,7 +40,6 @@ from .types.components import SelectMenu as SelectMenuPayload from .types.components import SelectOption as SelectOptionPayload - __all__ = ( "Component", "ActionRow", @@ -293,8 +292,15 @@ class SelectMenu(Component): .. versionadded:: 2.0 + .. versionchanged:: 2.3 + + Added support for :attr:`ComponentType.user_select`, :attr:`ComponentType.role_select`, + :attr:`ComponentType.mentionable_select`, and :attr:`ComponentType.channel_select`. + Attributes ---------- + type: :class:`ComponentType` + The select menu's type. custom_id: Optional[:class:`str`] The ID of the select menu that gets received during an interaction. placeholder: Optional[:class:`str`] @@ -307,6 +313,12 @@ class SelectMenu(Component): Defaults to 1 and must be between 1 and 25. options: List[:class:`SelectOption`] A list of options that can be selected in this menu. + Will be an empty list for all component types + except for :attr:`ComponentType.string_select`. + channel_types: List[:class:`ChannelType`] + A list of channel types that can be selected. + Will be an empty list for all component types + except for :attr:`ComponentType.channel_select`. disabled: :class:`bool` Whether the select is disabled or not. """ @@ -317,21 +329,25 @@ class SelectMenu(Component): "min_values", "max_values", "options", + "channel_types", "disabled", ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: SelectMenuPayload): - self.type = ComponentType.select + self.type = try_enum(ComponentType, data["type"]) self.custom_id: str = data["custom_id"] self.placeholder: str | None = data.get("placeholder") self.min_values: int = data.get("min_values", 1) self.max_values: int = data.get("max_values", 1) + self.disabled: bool = data.get("disabled", False) self.options: list[SelectOption] = [ SelectOption.from_dict(option) for option in data.get("options", []) ] - self.disabled: bool = data.get("disabled", False) + self.channel_types: list[ChannelType] = [ + try_enum(ChannelType, ct) for ct in data.get("channel_types", []) + ] def to_dict(self) -> SelectMenuPayload: payload: SelectMenuPayload = { @@ -339,10 +355,13 @@ def to_dict(self) -> SelectMenuPayload: "custom_id": self.custom_id, "min_values": self.min_values, "max_values": self.max_values, - "options": [op.to_dict() for op in self.options], "disabled": self.disabled, } + if self.type is ComponentType.string_select: + payload["options"] = [op.to_dict() for op in self.options] + if self.type is ComponentType.channel_select and self.channel_types: + payload["channel_types"] = [ct.value for ct in self.channel_types] if self.placeholder: payload["placeholder"] = self.placeholder @@ -350,7 +369,7 @@ def to_dict(self) -> SelectMenuPayload: class SelectOption: - """Represents a select menu's option. + """Represents a :class:`discord.SelectMenu`'s option. These can be created by users. @@ -406,7 +425,8 @@ def __init__( def __repr__(self) -> str: return ( - f"" ) @@ -418,7 +438,7 @@ def __str__(self) -> str: @property def emoji(self) -> str | Emoji | PartialEmoji | None: - """Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]]: The emoji of the option, if available.""" + """The emoji of the option, if available.""" return self._emoji @emoji.setter @@ -430,7 +450,8 @@ def emoji(self, value) -> None: value = value._to_partial() else: raise TypeError( - f"expected emoji to be str, Emoji, or PartialEmoji not {value.__class__}" + "expected emoji to be str, Emoji, or PartialEmoji not" + f" {value.__class__}" ) self._emoji = value @@ -472,7 +493,9 @@ def _component_factory(data: ComponentPayload) -> Component: return ActionRow(data) elif component_type == 2: return Button(data) # type: ignore - elif component_type == 3: + elif component_type == 4: + return InputText(data) # type: ignore + elif component_type in (3, 5, 6, 7, 8): return SelectMenu(data) # type: ignore else: as_enum = try_enum(ComponentType, component_type) diff --git a/discord/embeds.py b/discord/embeds.py index 8b7c9f32..ca3a9c5e 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -247,7 +247,6 @@ def __init__( timestamp: datetime.datetime = None, fields: list[EmbedField] | None = None, ): - self.colour = colour if colour is not EmptyEmbed else color self.title = title self.type = type @@ -402,7 +401,8 @@ def colour(self, value: int | Colour | _EmptyEmbed): # type: ignore self._colour = Colour(value=value) else: raise TypeError( - f"Expected discord.Colour, int, or Embed.Empty but received {value.__class__.__name__} instead." + "Expected discord.Colour, int, or Embed.Empty but received" + f" {value.__class__.__name__} instead." ) color = colour @@ -421,7 +421,8 @@ def timestamp(self, value: MaybeEmpty[datetime.datetime]): self._timestamp = value else: raise TypeError( - f"Expected datetime.datetime or Embed.Empty received {value.__class__.__name__} instead" + "Expected datetime.datetime or Embed.Empty received" + f" {value.__class__.__name__} instead" ) @property diff --git a/discord/emoji.py b/discord/emoji.py index 08dad90e..002aa158 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -139,7 +139,10 @@ def __str__(self) -> str: return f"<:{self.name}:{self.id}>" def __repr__(self) -> str: - return f"" + return ( + "" + ) def __eq__(self, other: Any) -> bool: return isinstance(other, _EmojiTag) and self.id == other.id @@ -152,18 +155,18 @@ def __hash__(self) -> int: @property def created_at(self) -> datetime: - """:class:`datetime.datetime`: Returns the emoji's creation time in UTC.""" + """Returns the emoji's creation time in UTC.""" return snowflake_time(self.id) @property def url(self) -> str: - """:class:`str`: Returns the URL of the emoji.""" + """Returns the URL of the emoji.""" fmt = "gif" if self.animated else "png" return f"{Asset.BASE}/emojis/{self.id}.{fmt}" @property def roles(self) -> list[Role]: - """List[:class:`Role`]: A :class:`list` of roles that is allowed to use this emoji. + """A :class:`list` of roles that is allowed to use this emoji. If roles is empty, the emoji is unrestricted. """ @@ -175,11 +178,11 @@ def roles(self) -> list[Role]: @property def guild(self) -> Guild: - """:class:`Guild`: The guild this emoji belongs to.""" + """The guild this emoji belongs to.""" return self._state._get_guild(self.guild_id) def is_usable(self) -> bool: - """:class:`bool`: Whether the bot can use this emoji. + """Whether the bot can use this emoji. .. versionadded:: 1.3 """ diff --git a/discord/enums.py b/discord/enums.py index dc952d0d..75af2443 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -22,20 +22,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations import types from collections import namedtuple -from typing import ( - TYPE_CHECKING, - Any, - ClassVar, - Dict, - List, - Optional, - Type, - TypeVar, - Union, -) +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, Union __all__ = ( "Enum", @@ -76,6 +67,7 @@ "AutoModEventType", "AutoModActionType", "AutoModKeywordPresetType", + "ApplicationRoleConnectionMetadataType", ) @@ -112,9 +104,9 @@ def _is_descriptor(obj): class EnumMeta(type): if TYPE_CHECKING: __name__: ClassVar[str] - _enum_member_names_: ClassVar[List[str]] - _enum_member_map_: ClassVar[Dict[str, Any]] - _enum_value_map_: ClassVar[Dict[Any, Any]] + _enum_member_names_: ClassVar[list[str]] + _enum_member_map_: ClassVar[dict[str, Any]] + _enum_value_map_: ClassVar[dict[Any, Any]] def __new__(cls, name, bases, attrs, *, comparable: bool = False): value_mapping = {} @@ -210,6 +202,8 @@ def try_value(cls, value): class ChannelType(Enum): + """Channel type""" + text = 0 private = 1 voice = 2 @@ -228,6 +222,8 @@ def __str__(self): class MessageType(Enum): + """Message type""" + default = 0 recipient_add = 1 recipient_remove = 2 @@ -253,9 +249,19 @@ class MessageType(Enum): guild_invite_reminder = 22 context_menu_command = 23 auto_moderation_action = 24 + role_subscription_purchase = 25 + interaction_premium_upsell = 26 + stage_start = 27 + stage_end = 28 + stage_speaker = 29 + stage_raise_hand = 30 + stage_topic = 31 + guild_application_premium_subscription = 32 class VoiceRegion(Enum): + """Voice region""" + us_west = "us-west" us_east = "us-east" us_south = "us-south" @@ -285,6 +291,8 @@ def __str__(self): class SpeakingState(Enum): + """Speaking state""" + none = 0 voice = 1 soundshare = 2 @@ -298,6 +306,8 @@ def __int__(self): class VerificationLevel(Enum, comparable=True): + """Verification level""" + none = 0 low = 1 medium = 2 @@ -308,7 +318,19 @@ def __str__(self): return self.name +class SortOrder(Enum): + """Forum Channel Sort Order""" + + latest_activity = 0 + creation_date = 1 + + def __str__(self): + return self.name + + class ContentFilter(Enum, comparable=True): + """Content Filter""" + disabled = 0 no_role = 1 all_members = 2 @@ -318,6 +340,8 @@ def __str__(self): class Status(Enum): + """Status""" + online = "online" offline = "offline" idle = "idle" @@ -331,6 +355,8 @@ def __str__(self): class DefaultAvatar(Enum): + """Default avatar""" + blurple = 0 grey = 1 gray = 1 @@ -343,17 +369,23 @@ def __str__(self): class NotificationLevel(Enum, comparable=True): + """Notification level""" + all_messages = 0 only_mentions = 1 class AuditLogActionCategory(Enum): + """Audit log action category""" + create = 1 delete = 2 update = 3 class AuditLogAction(Enum): + """Audit log action""" + guild_update = 1 channel_create = 10 channel_update = 11 @@ -408,8 +440,8 @@ class AuditLogAction(Enum): auto_moderation_block_message = 143 @property - def category(self) -> Optional[AuditLogActionCategory]: - lookup: Dict[AuditLogAction, Optional[AuditLogActionCategory]] = { + def category(self) -> AuditLogActionCategory | None: + lookup: dict[AuditLogAction, AuditLogActionCategory | None] = { AuditLogAction.guild_update: AuditLogActionCategory.update, AuditLogAction.channel_create: AuditLogActionCategory.create, AuditLogAction.channel_update: AuditLogActionCategory.update, @@ -457,7 +489,9 @@ def category(self) -> Optional[AuditLogActionCategory]: AuditLogAction.thread_create: AuditLogActionCategory.create, AuditLogAction.thread_update: AuditLogActionCategory.update, AuditLogAction.thread_delete: AuditLogActionCategory.delete, - AuditLogAction.application_command_permission_update: AuditLogActionCategory.update, + AuditLogAction.application_command_permission_update: ( + AuditLogActionCategory.update + ), AuditLogAction.auto_moderation_rule_create: AuditLogActionCategory.create, AuditLogAction.auto_moderation_rule_update: AuditLogActionCategory.update, AuditLogAction.auto_moderation_rule_delete: AuditLogActionCategory.delete, @@ -466,7 +500,7 @@ def category(self) -> Optional[AuditLogActionCategory]: return lookup[self] @property - def target_type(self) -> Optional[str]: + def target_type(self) -> str | None: v = self.value if v == -1: return "all" @@ -505,6 +539,8 @@ def target_type(self) -> Optional[str]: class UserFlags(Enum): + """User flags""" + staff = 1 partner = 2 hypesquad = 4 @@ -526,9 +562,12 @@ class UserFlags(Enum): discord_certified_moderator = 262144 bot_http_interactions = 524288 spammer = 1048576 + active_developer = 4194304 class ActivityType(Enum): + """Activity type""" + unknown = -1 playing = 0 streaming = 1 @@ -542,17 +581,23 @@ def __int__(self): class TeamMembershipState(Enum): + """Team membership state""" + invited = 1 accepted = 2 class WebhookType(Enum): + """Webhook Type""" + incoming = 1 channel_follower = 2 application = 3 class ExpireBehaviour(Enum): + """Expire Behaviour""" + remove_role = 0 kick = 1 @@ -561,32 +606,43 @@ class ExpireBehaviour(Enum): class StickerType(Enum): + """Sticker type""" + standard = 1 guild = 2 class StickerFormatType(Enum): + """Sticker format Type""" + png = 1 apng = 2 lottie = 3 + gif = 4 @property def file_extension(self) -> str: - lookup: Dict[StickerFormatType, str] = { + lookup: dict[StickerFormatType, str] = { StickerFormatType.png: "png", StickerFormatType.apng: "png", StickerFormatType.lottie: "json", + StickerFormatType.gif: "gif", } - return lookup[self] + # TODO: Improve handling of unknown sticker format types if possible + return lookup.get(self, "png") class InviteTarget(Enum): + """Invite target""" + unknown = 0 stream = 1 embedded_application = 2 class InteractionType(Enum): + """Interaction type""" + ping = 1 application_command = 2 component = 3 @@ -595,6 +651,8 @@ class InteractionType(Enum): class InteractionResponseType(Enum): + """Interaction response type""" + pong = 1 # ack = 2 (deprecated) # channel_message = 3 (deprecated) @@ -607,6 +665,8 @@ class InteractionResponseType(Enum): class VideoQualityMode(Enum): + """Video quality mode""" + auto = 1 full = 2 @@ -615,16 +675,25 @@ def __int__(self): class ComponentType(Enum): + """Component type""" + action_row = 1 button = 2 - select = 3 + string_select = 3 + select = string_select # (deprecated) alias for string_select input_text = 4 + user_select = 5 + role_select = 6 + mentionable_select = 7 + channel_select = 8 def __int__(self): return self.value class ButtonStyle(Enum): + """Button style""" + primary = 1 secondary = 2 success = 3 @@ -644,6 +713,8 @@ def __int__(self): class InputTextStyle(Enum): + """Input text style""" + short = 1 singleline = 1 paragraph = 2 @@ -652,6 +723,8 @@ class InputTextStyle(Enum): class ApplicationType(Enum): + """Application type""" + game = 1 music = 2 ticketed_events = 3 @@ -659,12 +732,16 @@ class ApplicationType(Enum): class StagePrivacyLevel(Enum): + """Stage privacy level""" + # public = 1 (deprecated) closed = 2 guild_only = 2 class NSFWLevel(Enum, comparable=True): + """NSFW level""" + default = 0 explicit = 1 safe = 2 @@ -672,6 +749,8 @@ class NSFWLevel(Enum, comparable=True): class SlashCommandOptionType(Enum): + """Slash command option type""" + sub_command = 1 sub_command_group = 2 string = 3 @@ -714,6 +793,8 @@ def from_datatype(cls, datatype): "CategoryChannel", "ThreadOption", "Thread", + "ForumChannel", + "DMChannel", ]: return cls.channel if datatype.__name__ == "Role": @@ -743,32 +824,49 @@ def from_datatype(cls, datatype): class EmbeddedActivity(Enum): + """Embedded activity""" + + ask_away = 976052223358406656 awkword = 879863881349087252 + awkword_dev = 879863923543785532 + bash_out = 1006584476094177371 betrayal = 773336526917861400 + blazing_8s = 832025144389533716 + blazing_8s_dev = 832013108234289153 + blazing_8s_qa = 832025114077298718 + blazing_8s_staging = 832025061657280566 + bobble_league = 947957217959759964 checkers_in_the_park = 832013003968348200 checkers_in_the_park_dev = 832012682520428625 - checkers_in_the_park_staging = 832012938398400562 checkers_in_the_park_qa = 832012894068801636 + checkers_in_the_park_staging = 832012938398400562 chess_in_the_park = 832012774040141894 chess_in_the_park_dev = 832012586023256104 - chest_in_the_park_staging = 832012730599735326 - chest_in_the_park_qa = 832012815819604009 + chess_in_the_park_qa = 832012815819604009 + chess_in_the_park_staging = 832012730599735326 decoders_dev = 891001866073296967 doodle_crew = 878067389634314250 doodle_crew_dev = 878067427668275241 fishington = 814288819477020702 - letter_tile = 879863686565621790 - ocho = 832025144389533716 - ocho_dev = 832013108234289153 - ocho_staging = 832025061657280566 - ocho_qa = 832025114077298718 + know_what_i_meme = 950505761862189096 + land = 903769130790969345 + letter_league = 879863686565621790 + letter_league_dev = 879863753519292467 poker_night = 755827207812677713 - poker_night_staging = 763116274876022855 + poker_night_dev = 763133495793942528 poker_night_qa = 801133024841957428 + poker_night_staging = 763116274876022855 + putt_party = 945737671223947305 + putt_party_dev = 910224161476083792 + putt_party_qa = 945748195256979606 + putt_party_staging = 945732077960188005 putts = 832012854282158180 + sketch_heads = 902271654783242291 + sketch_heads_dev = 902271746701414431 sketchy_artist = 879864070101172255 sketchy_artist_dev = 879864104980979792 spell_cast = 852509694341283871 + spell_cast_staging = 893449443918086174 watch_together = 880218394199220334 watch_together_dev = 880218832743055411 word_snacks = 879863976006127627 @@ -777,6 +875,8 @@ class EmbeddedActivity(Enum): class ScheduledEventStatus(Enum): + """Scheduled event status""" + scheduled = 1 active = 2 completed = 3 @@ -788,6 +888,8 @@ def __int__(self): class ScheduledEventPrivacyLevel(Enum): + """Scheduled event privacy level""" + guild_only = 2 def __int__(self): @@ -795,44 +897,68 @@ def __int__(self): class ScheduledEventLocationType(Enum): + """Scheduled event location type""" + stage_instance = 1 voice = 2 external = 3 class AutoModTriggerType(Enum): + """Automod trigger type""" + keyword = 1 harmful_link = 2 spam = 3 keyword_preset = 4 + mention_spam = 5 class AutoModEventType(Enum): + """Automod event type""" + message_send = 1 class AutoModActionType(Enum): + """Automod action type""" + block_message = 1 send_alert_message = 2 timeout = 3 class AutoModKeywordPresetType(Enum): + """Automod keyword preset type""" + profanity = 1 sexual_content = 2 slurs = 3 +class ApplicationRoleConnectionMetadataType(Enum): + """Application role connection metadata type""" + + integer_less_than_or_equal = 1 + integer_greater_than_or_equal = 2 + integer_equal = 3 + integer_not_equal = 4 + datetime_less_than_or_equal = 5 + datetime_greater_than_or_equal = 6 + boolean_equal = 7 + boolean_not_equal = 8 + + T = TypeVar("T") -def create_unknown_value(cls: Type[T], val: Any) -> T: +def create_unknown_value(cls: type[T], val: Any) -> T: value_cls = cls._enum_value_cls_ # type: ignore name = f"unknown_{val}" return value_cls(name=name, value=val) -def try_enum(cls: Type[T], val: Any) -> T: +def try_enum(cls: type[T], val: Any) -> T: """A function that tries to turn the value into enum ``cls``. If it fails it returns a proxy invalid value instead. diff --git a/discord/errors.py b/discord/errors.py index 0d13944d..589f4f20 100644 --- a/discord/errors.py +++ b/discord/errors.py @@ -255,10 +255,11 @@ class PrivilegedIntentsRequired(ClientException): def __init__(self, shard_id: int | None): self.shard_id: int | None = shard_id msg = ( - "Shard ID %s is requesting privileged intents that have not been explicitly enabled in the " - "developer portal. It is recommended to go to https://discord.com/developers/applications/ " - "and explicitly enable the privileged intents within your application's page. If this is not " - "possible, then consider disabling the privileged intents instead." + "Shard ID %s is requesting privileged intents that have not been explicitly" + " enabled in the developer portal. It is recommended to go to" + " https://discord.com/developers/applications/ and explicitly enable the" + " privileged intents within your application's page. If this is not" + " possible, then consider disabling the privileged intents instead." ) super().__init__(msg % shard_id) @@ -349,7 +350,10 @@ class ExtensionFailed(ExtensionError): def __init__(self, name: str, original: Exception) -> None: self.original: Exception = original - msg = f"Extension {name!r} raised an error: {original.__class__.__name__}: {original}" + msg = ( + f"Extension {name!r} raised an error: {original.__class__.__name__}:" + f" {original}" + ) super().__init__(msg, name=name) diff --git a/discord/ext/bridge/bot.py b/discord/ext/bridge/bot.py index 3db78d5f..38fea36b 100644 --- a/discord/ext/bridge/bot.py +++ b/discord/ext/bridge/bot.py @@ -22,6 +22,8 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations + from abc import ABC from discord.interactions import Interaction @@ -36,6 +38,17 @@ class BotBase(ABC): + _bridge_commands: list[BridgeCommand | BridgeCommandGroup] + + @property + def bridge_commands(self) -> list[BridgeCommand | BridgeCommandGroup]: + """Returns all of the bot's bridge commands.""" + + if not (cmds := getattr(self, "_bridge_commands", None)): + self._bridge_commands = cmds = [] + + return cmds + async def get_application_context( self, interaction: Interaction, cls=None ) -> BridgeApplicationContext: @@ -57,6 +70,8 @@ def add_bridge_command(self, command: BridgeCommand): # Ignore the type hinting error here. All subclasses of BotBase pass the type checks. command.add_to(self) # type: ignore + self.bridge_commands.append(command) + def bridge_command(self, **kwargs): """A shortcut decorator that invokes :func:`bridge_command` and adds it to the internal command list via :meth:`~.Bot.add_bridge_command`. diff --git a/discord/ext/bridge/context.py b/discord/ext/bridge/context.py index 80355720..3ba59898 100644 --- a/discord/ext/bridge/context.py +++ b/discord/ext/bridge/context.py @@ -126,7 +126,7 @@ def _get_super(self, attr: str) -> Any: @property def is_app(self) -> bool: - """bool: Whether the context is an :class:`BridgeApplicationContext` or not.""" + """Whether the context is an :class:`BridgeApplicationContext` or not.""" return isinstance(self, BridgeApplicationContext) diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py index f305615d..22fad26f 100644 --- a/discord/ext/bridge/core.py +++ b/discord/ext/bridge/core.py @@ -68,6 +68,7 @@ "map_to", "guild_only", "has_permissions", + "is_nsfw", ) @@ -75,7 +76,7 @@ class BridgeSlashCommand(SlashCommand): """A subclass of :class:`.SlashCommand` that is used for bridge commands.""" def __init__(self, func, **kwargs): - kwargs = filter_params(kwargs, brief="description") + self.brief = kwargs.pop("brief", None) super().__init__(func, **kwargs) @@ -83,7 +84,6 @@ class BridgeExtCommand(Command): """A subclass of :class:`.ext.commands.Command` that is used for bridge commands.""" def __init__(self, func, **kwargs): - kwargs = filter_params(kwargs, description="brief") super().__init__(func, **kwargs) async def transform(self, ctx: Context, param: inspect.Parameter) -> Any: @@ -100,6 +100,8 @@ class BridgeSlashGroup(SlashCommandGroup): __slots__ = ("module",) def __init__(self, callback, *args, **kwargs): + if perms := getattr(callback, "__default_member_permissions__", None): + kwargs["default_member_permissions"] = perms super().__init__(*args, **kwargs) self.callback = callback self.__original_kwargs__["callback"] = callback @@ -154,8 +156,8 @@ def __init__(self, callback, **kwargs): ) or BridgeExtCommand(callback, **kwargs) @property - def name_localizations(self): - """Dict[:class:`str`, :class:`str`]: Returns name_localizations from :attr:`slash_variant` + def name_localizations(self) -> dict[str, str]: + """Returns name_localizations from :attr:`slash_variant` You can edit/set name_localizations directly with @@ -172,8 +174,8 @@ def name_localizations(self, value): self.slash_variant.name_localizations = value @property - def description_localizations(self): - """Dict[:class:`str`, :class:`str`]: Returns description_localizations from :attr:`slash_variant` + def description_localizations(self) -> dict[str, str]: + """Returns description_localizations from :attr:`slash_variant` You can edit/set description_localizations directly with @@ -189,6 +191,10 @@ def description_localizations(self): def description_localizations(self, value): self.slash_variant.description_localizations = value + @property + def qualified_name(self) -> str: + return self.slash_variant.qualified_name + def add_to(self, bot: ExtBot) -> None: """Adds the command to a bot. This method is inherited by :class:`.BridgeCommandGroup`. @@ -305,12 +311,17 @@ class BridgeCommandGroup(BridgeCommand): If :func:`map_to` is used, the mapped slash command. """ + ext_variant: BridgeExtGroup + slash_variant: BridgeSlashGroup + def __init__(self, callback, *args, **kwargs): - self.ext_variant: BridgeExtGroup = BridgeExtGroup(callback, *args, **kwargs) - name = kwargs.pop("name", self.ext_variant.name) - self.slash_variant: BridgeSlashGroup = BridgeSlashGroup( - callback, name, *args, **kwargs + super().__init__( + callback, + ext_variant=(ext_var := BridgeExtGroup(callback, *args, **kwargs)), + slash_variant=BridgeSlashGroup(callback, ext_var.name, *args, **kwargs), + parent=kwargs.pop("parent", None), ) + self.subcommands: list[BridgeCommand] = [] self.mapped: SlashCommand | None = None @@ -330,12 +341,12 @@ def command(self, *args, **kwargs): def wrap(callback): slash = self.slash_variant.command( *args, - **filter_params(kwargs, brief="description"), + **kwargs, cls=BridgeSlashCommand, )(callback) ext = self.ext_variant.command( *args, - **filter_params(kwargs, description="brief"), + **kwargs, cls=BridgeExtCommand, )(callback) command = BridgeCommand( @@ -436,6 +447,30 @@ def predicate(func: Callable | ApplicationCommand): return predicate +def is_nsfw(): + """Intended to work with :class:`.ApplicationCommand` and :class:`BridgeCommand`, adds a :func:`~ext.commands.check` + that locks the command to only run in nsfw contexts, and also registers the command as nsfw client-side (on discord). + + Basically a utility function that wraps both :func:`discord.ext.commands.is_nsfw` and :func:`discord.commands.is_nsfw`. + + .. warning:: + + In DMs, the prefixed-based command will always run as the user's privacy settings cannot be checked directly. + """ + + def predicate(func: Callable | ApplicationCommand): + if isinstance(func, ApplicationCommand): + func.nsfw = True + else: + func.__nsfw__ = True + + from ..commands import is_nsfw + + return is_nsfw()(func) + + return predicate + + def has_permissions(**perms: dict[str, bool]): r"""Intended to work with :class:`.SlashCommand` and :class:`BridgeCommand`, adds a :func:`~ext.commands.check` that locks the command to be run by people with certain @@ -454,13 +489,13 @@ def predicate(func: Callable | ApplicationCommand): from ..commands import has_permissions func = has_permissions(**perms)(func) - Permissions(**perms) + _perms = Permissions(**perms) if isinstance(func, ApplicationCommand): - func.default_member_permissions = perms + func.default_member_permissions = _perms else: - func.__default_member_permissions__ = perms + func.__default_member_permissions__ = _perms - return perms + return func return predicate @@ -522,7 +557,8 @@ async def convert(self, ctx, argument: str) -> Any: choices = [choice.value for choice in self.choices] if converted not in choices: raise ValueError( - f"{argument} is not a valid choice. Valid choices: {list(set(choices_names + choices))}" + f"{argument} is not a valid choice. Valid choices:" + f" {list(set(choices_names + choices))}" ) return converted diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 5c3cb249..ae8a48c2 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -238,8 +238,8 @@ async def get_prefix(self, message: Message) -> list[str] | str: raise raise TypeError( - "command_prefix must be plain string, iterable of strings, or callable " - f"returning either of these, not {ret.__class__.__name__}" + "command_prefix must be plain string, iterable of strings, or" + f" callable returning either of these, not {ret.__class__.__name__}" ) if not ret: @@ -311,8 +311,9 @@ class be provided, it must be similar enough to :class:`.Context`\'s for value in prefix: if not isinstance(value, str): raise TypeError( - "Iterable command_prefix or list returned from get_prefix must " - f"contain only strings, not {value.__class__.__name__}" + "Iterable command_prefix or list returned from get_prefix" + " must contain only strings, not" + f" {value.__class__.__name__}" ) # Getting here shouldn't happen diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index f6a9669c..08d0f9f0 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -249,7 +249,7 @@ async def reinvoke(self, *, call_hooks: bool = False, restart: bool = True) -> N @property def valid(self) -> bool: - """:class:`bool`: Checks if the invocation context is valid to be invoked with.""" + """Checks if the invocation context is valid to be invoked with.""" return self.prefix is not None and self.command is not None async def _get_channel(self) -> discord.abc.Messageable: @@ -257,7 +257,7 @@ async def _get_channel(self) -> discord.abc.Messageable: @property def clean_prefix(self) -> str: - """:class:`str`: The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``. + """The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``. .. versionadded:: 2.0 """ @@ -274,7 +274,7 @@ def clean_prefix(self) -> str: @property def cog(self) -> Cog | None: - """Optional[:class:`.Cog`]: Returns the cog associated with this context's command. + """Returns the cog associated with this context's command. None if it does not exist. """ @@ -284,14 +284,14 @@ def cog(self) -> Cog | None: @discord.utils.cached_property def guild(self) -> Guild | None: - """Optional[:class:`.Guild`]: Returns the guild associated with this context's command. + """Returns the guild associated with this context's command. None if not available. """ return self.message.guild @discord.utils.cached_property def channel(self) -> MessageableChannel: - """Union[:class:`.abc.Messageable`]: Returns the channel associated with this context's command. + """Returns the channel associated with this context's command. Shorthand for :attr:`.Message.channel`. """ return self.message.channel @@ -314,7 +314,7 @@ def me(self) -> Member | ClientUser: @property def voice_client(self) -> VoiceProtocol | None: - r"""Optional[:class:`.VoiceProtocol`]: A shortcut to :attr:`.Guild.voice_client`\, if applicable.""" + r"""A shortcut to :attr:`.Guild.voice_client`\, if applicable.""" g = self.guild return g.voice_client if g else None diff --git a/discord/ext/commands/cooldowns.py b/discord/ext/commands/cooldowns.py index 90f387e9..55792aba 100644 --- a/discord/ext/commands/cooldowns.py +++ b/discord/ext/commands/cooldowns.py @@ -192,7 +192,10 @@ def copy(self) -> Cooldown: return Cooldown(self.rate, self.per) def __repr__(self) -> str: - return f"" + return ( + f"" + ) class CooldownMapping: @@ -291,7 +294,7 @@ class _Semaphore: `wait=False`. An asyncio.Queue could have been used to do this as well -- but it is - not as inefficient since internally that uses two queues and is a bit + not as efficient since internally that uses two queues and is a bit overkill for what is basically a counter. """ diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index eee8d8e9..d3ce2e07 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -679,7 +679,7 @@ def clean_params(self) -> dict[str, inspect.Parameter]: @property def full_parent_name(self) -> str: - """:class:`str`: Retrieves the fully qualified parent command name. + """Retrieves the fully qualified parent command name. This the base command name required to execute it. For example, in ``?one two three`` the parent name would be ``one two``. @@ -695,7 +695,7 @@ def full_parent_name(self) -> str: @property def parents(self) -> list[Group]: - """List[:class:`Group`]: Retrieves the parents of this command. + """Retrieves the parents of this command. If the command has no parents then it returns an empty :class:`list`. @@ -713,7 +713,7 @@ def parents(self) -> list[Group]: @property def root_parent(self) -> Group | None: - """Optional[:class:`Group`]: Retrieves the root parent of this command. + """Retrieves the root parent of this command. If the command has no parents then it returns ``None``. @@ -725,7 +725,7 @@ def root_parent(self) -> Group | None: @property def qualified_name(self) -> str: - """:class:`str`: Retrieves the fully qualified command name. + """Retrieves the fully qualified command name. This is the full parent name with the command name as well. For example, in ``?one two three`` the qualified name would be @@ -991,7 +991,7 @@ def error(self, coro: ErrorT) -> ErrorT: return coro def has_error_handler(self) -> bool: - """:class:`bool`: Checks whether the command has an error handler registered. + """Checks whether the command has an error handler registered. .. versionadded:: 1.7 """ @@ -1053,12 +1053,12 @@ def after_invoke(self, coro: HookT) -> HookT: @property def cog_name(self) -> str | None: - """Optional[:class:`str`]: The name of the cog this command belongs to, if any.""" + """The name of the cog this command belongs to, if any.""" return type(self.cog).__cog_name__ if self.cog is not None else None @property def short_doc(self) -> str: - """:class:`str`: Gets the "short" documentation of a command. + """Gets the "short" documentation of a command. By default, this is the :attr:`.brief` attribute. If that lookup leads to an empty string then the first line of the @@ -1080,7 +1080,7 @@ def _is_typing_optional(self, annotation: T | T | None) -> TypeGuard[T | None]: @property def signature(self) -> str: - """:class:`str`: Returns a POSIX-like signature useful for help command output.""" + """Returns a POSIX-like signature useful for help command output.""" if self.usage is not None: return self.usage @@ -1178,7 +1178,8 @@ async def can_run(self, ctx: Context) -> bool: try: if not await ctx.bot.can_run(ctx): raise CheckFailure( - f"The global check functions for command {self.qualified_name} failed." + "The global check functions for command" + f" {self.qualified_name} failed." ) cog = self.cog @@ -1232,7 +1233,7 @@ def all_commands(self): @property def commands(self) -> set[Command[CogT, Any, Any]]: - """Set[:class:`.Command`]: A unique set of commands without aliases that are registered.""" + """A unique set of commands without aliases that are registered.""" return set(self.prefixed_commands.values()) def recursively_remove_all_commands(self) -> None: @@ -2009,9 +2010,11 @@ def predicate(ctx): # ctx.guild is None doesn't narrow ctx.author to Member getter = functools.partial(discord.utils.get, ctx.author.roles) # type: ignore if any( - getter(id=item) is not None - if isinstance(item, int) - else getter(name=item) is not None + ( + getter(id=item) is not None + if isinstance(item, int) + else getter(name=item) is not None + ) for item in items ): return True @@ -2071,9 +2074,11 @@ def predicate(ctx): me = ctx.me getter = functools.partial(discord.utils.get, me.roles) if any( - getter(id=item) is not None - if isinstance(item, int) - else getter(name=item) is not None + ( + getter(id=item) is not None + if isinstance(item, int) + else getter(name=item) is not None + ) for item in items ): return True diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index 955f7fab..c9174054 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -593,7 +593,8 @@ def __init__(self, number: int, per: BucketType) -> None: plural = "%s times %s" if number > 1 else "%s time %s" fmt = plural % (number, suffix) super().__init__( - f"Too many people are using this command. It can only be used {fmt} concurrently." + "Too many people are using this command. It can only be used" + f" {fmt} concurrently." ) @@ -957,7 +958,8 @@ def __init__(self, flag: Flag, values: list[str]) -> None: self.flag: Flag = flag self.values: list[str] = values super().__init__( - f"Too many flag values, expected {flag.max_args} but received {len(values)}." + f"Too many flag values, expected {flag.max_args} but received" + f" {len(values)}." ) diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 82183fb3..d265442b 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -31,7 +31,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Iterator, Literal, Pattern, TypeVar, Union -from discord.utils import MISSING, maybe_coroutine, resolve_annotation +from discord.utils import MISSING, MissingField, maybe_coroutine, resolve_annotation from .converter import run_converters from .errors import ( @@ -81,18 +81,18 @@ class Flag: Whether multiple given values overrides the previous value. """ - name: str = MISSING + name: str = MissingField aliases: list[str] = field(default_factory=list) - attribute: str = MISSING - annotation: Any = MISSING - default: Any = MISSING - max_args: int = MISSING - override: bool = MISSING + attribute: str = MissingField + annotation: Any = MissingField + default: Any = MissingField + max_args: int = MissingField + override: bool = MissingField cast_to_dict: bool = False @property def required(self) -> bool: - """:class:`bool`: Whether the flag is required. + """Whether the flag is required. A required flag has no default value. """ @@ -231,7 +231,8 @@ def get_flags( flag.max_args = 1 else: raise TypeError( - f"Unsupported typing annotation {annotation!r} for {flag.name!r} flag" + f"Unsupported typing annotation {annotation!r} for" + f" {flag.name!r} flag" ) if flag.override is MISSING: @@ -251,7 +252,8 @@ def get_flags( alias = alias.casefold() if case_insensitive else alias if alias in names: raise TypeError( - f"{flag.name!r} flag alias {alias!r} conflicts with previous flag or alias." + f"{flag.name!r} flag alias {alias!r} conflicts with previous flag" + " or alias." ) else: names.add(alias) @@ -495,7 +497,7 @@ class FlagConverter(metaclass=FlagsMeta): @classmethod def get_flags(cls) -> dict[str, Flag]: - """Dict[:class:`str`, :class:`Flag`]: A mapping of flag name to flag object this converter has.""" + """A mapping of flag name to flag object this converter has.""" return cls.__commands_flags__.copy() @classmethod diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py index 1bf077cb..90899ef6 100644 --- a/discord/ext/commands/help.py +++ b/discord/ext/commands/help.py @@ -23,6 +23,8 @@ DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations + import copy import functools import itertools @@ -172,8 +174,8 @@ def __len__(self): return total + self._count @property - def pages(self): - """List[:class:`str`]: Returns the rendered list of pages.""" + def pages(self) -> list[str]: + """Returns the rendered list of pages.""" # we have more than just the prefix in our current page if len(self._current_page) > (0 if self.prefix is None else 1): self.close_page() @@ -181,8 +183,8 @@ def pages(self): def __repr__(self): return ( - f"" + f"" ) @@ -274,7 +276,7 @@ class HelpCommand: Internally instances of this class are deep copied every time the command itself is invoked to prevent a race condition - mentioned in :issue:`2123`. + mentioned in :dpy-issue:`2123`. This means that relying on the state of this class to be the same between command invocations would not work as expected. @@ -947,18 +949,20 @@ def __init__(self, **options): super().__init__(**options) - def shorten_text(self, text): - """:class:`str`: Shortens text to fit into the :attr:`width`.""" + def shorten_text(self, text: str) -> str: + """Shortens text to fit into the :attr:`width`.""" if len(text) > self.width: return f"{text[:self.width - 3].rstrip()}..." return text - def get_ending_note(self): - """:class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes.""" + def get_ending_note(self) -> str: + """Returns help command's ending note. This is mainly useful to override for i18n purposes.""" command_name = self.invoked_with return ( - f"Type {self.context.clean_prefix}{command_name} command for more info on a command.\n" - f"You can also type {self.context.clean_prefix}{command_name} category for more info on a category." + f"Type {self.context.clean_prefix}{command_name} command for more info on a" + " command.\nYou can also type" + f" {self.context.clean_prefix}{command_name} category for more info on a" + " category." ) def add_indented_commands(self, commands, *, heading, max_size=None): @@ -1176,8 +1180,10 @@ def get_opening_note(self): """ command_name = self.invoked_with return ( - f"Use `{self.context.clean_prefix}{command_name} [command]` for more info on a command.\n" - f"You can also use `{self.context.clean_prefix}{command_name} [category]` for more info on a category." + f"Use `{self.context.clean_prefix}{command_name} [command]` for more info" + " on a command.\nYou can also use" + f" `{self.context.clean_prefix}{command_name} [category]` for more info on" + " a category." ) def get_command_signature(self, command): diff --git a/discord/ext/commands/view.py b/discord/ext/commands/view.py index 2b4ff7ab..c8c7cd19 100644 --- a/discord/ext/commands/view.py +++ b/discord/ext/commands/view.py @@ -194,4 +194,7 @@ def get_quoted_word(self): result.append(current) def __repr__(self): - return f"" + return ( + f"" + ) diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py index 07e2b7af..46399298 100644 --- a/discord/ext/pages/pagination.py +++ b/discord/ext/pages/pagination.py @@ -21,7 +21,9 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from typing import Dict, List, Optional, Union +from __future__ import annotations + +from typing import List import discord from discord.ext.bridge import BridgeContext @@ -65,7 +67,7 @@ def __init__( self, button_type: str, label: str = None, - emoji: Union[str, discord.Emoji, discord.PartialEmoji] = None, + emoji: str | discord.Emoji | discord.PartialEmoji = None, style: discord.ButtonStyle = discord.ButtonStyle.green, disabled: bool = False, custom_id: str = None, @@ -82,7 +84,7 @@ def __init__( ) self.button_type = button_type self.label = label if label or emoji else button_type.capitalize() - self.emoji: Union[str, discord.Emoji, discord.PartialEmoji] = emoji + self.emoji: str | discord.Emoji | discord.PartialEmoji = emoji self.style = style self.disabled = disabled self.loop_label = self.label if not loop_label else loop_label @@ -139,10 +141,10 @@ class Page: def __init__( self, - content: Optional[str] = None, - embeds: Optional[List[Union[List[discord.Embed], discord.Embed]]] = None, - custom_view: Optional[discord.ui.View] = None, - files: Optional[List[discord.File]] = None, + content: str | None = None, + embeds: list[list[discord.Embed] | discord.Embed] | None = None, + custom_view: discord.ui.View | None = None, + files: list[discord.File] | None = None, **kwargs, ): if content is None and embeds is None: @@ -154,7 +156,7 @@ def __init__( self._custom_view = custom_view self._files = files or [] - async def callback(self, interaction: Optional[discord.Interaction] = None): + async def callback(self, interaction: discord.Interaction | None = None): """|coro| The coroutine associated to a specific page. If `Paginator.page_action()` is used, this coroutine is called. @@ -165,57 +167,54 @@ async def callback(self, interaction: Optional[discord.Interaction] = None): The interaction associated with the callback, if any. """ - def update_files(self) -> Optional[List[discord.File]]: - """Updates the files associated with the page by re-uploading them. - Typically used when the page is changed. + def update_files(self) -> list[discord.File] | None: + """Updates :class:`discord.File` objects so that they can be sent multiple + times. This is called internally each time the page is sent. """ for file in self._files: - with open(file.fp.name, "rb") as fp: # type: ignore - self._files[self._files.index(file)] = discord.File( - fp, # type: ignore - filename=file.filename, - description=file.description, - spoiler=file.spoiler, - ) + if file.fp.closed and (fn := getattr(file.fp, "name", None)): + file.fp = open(fn, "rb") + file.reset() + file.fp.close = lambda: None return self._files @property - def content(self) -> Optional[str]: + def content(self) -> str | None: """Gets the content for the page.""" return self._content @content.setter - def content(self, value: Optional[str]): + def content(self, value: str | None): """Sets the content for the page.""" self._content = value @property - def embeds(self) -> Optional[List[Union[List[discord.Embed], discord.Embed]]]: + def embeds(self) -> list[list[discord.Embed] | discord.Embed] | None: """Gets the embeds for the page.""" return self._embeds @embeds.setter - def embeds(self, value: Optional[List[Union[List[discord.Embed], discord.Embed]]]): + def embeds(self, value: list[list[discord.Embed] | discord.Embed] | None): """Sets the embeds for the page.""" self._embeds = value @property - def custom_view(self) -> Optional[discord.ui.View]: + def custom_view(self) -> discord.ui.View | None: """Gets the custom view assigned to the page.""" return self._custom_view @custom_view.setter - def custom_view(self, value: Optional[discord.ui.View]): + def custom_view(self, value: discord.ui.View | None): """Assigns a custom view to be shown when the page is displayed.""" self._custom_view = value @property - def files(self) -> Optional[List[discord.File]]: + def files(self) -> list[discord.File] | None: """Gets the files associated with the page.""" return self._files @files.setter - def files(self, value: Optional[List[discord.File]]): + def files(self, value: list[discord.File] | None): """Sets the files associated with the page.""" self._files = value @@ -272,32 +271,28 @@ class PageGroup: def __init__( self, - pages: Union[ - List[str], List[Page], List[Union[List[discord.Embed], discord.Embed]] - ], + pages: (list[str] | list[Page] | list[list[discord.Embed] | discord.Embed]), label: str, - description: Optional[str] = None, - emoji: Union[str, discord.Emoji, discord.PartialEmoji] = None, - default: Optional[bool] = None, - show_disabled: Optional[bool] = None, - show_indicator: Optional[bool] = None, - author_check: Optional[bool] = None, - disable_on_timeout: Optional[bool] = None, - use_default_buttons: Optional[bool] = None, + description: str | None = None, + emoji: str | discord.Emoji | discord.PartialEmoji = None, + default: bool | None = None, + show_disabled: bool | None = None, + show_indicator: bool | None = None, + author_check: bool | None = None, + disable_on_timeout: bool | None = None, + use_default_buttons: bool | None = None, default_button_row: int = 0, - loop_pages: Optional[bool] = None, - custom_view: Optional[discord.ui.View] = None, - timeout: Optional[float] = None, - custom_buttons: Optional[List[PaginatorButton]] = None, - trigger_on_display: Optional[bool] = None, + loop_pages: bool | None = None, + custom_view: discord.ui.View | None = None, + timeout: float | None = None, + custom_buttons: list[PaginatorButton] | None = None, + trigger_on_display: bool | None = None, ): self.label = label - self.description: Optional[str] = description - self.emoji: Union[str, discord.Emoji, discord.PartialEmoji] = emoji - self.pages: Union[ - List[str], List[Union[List[discord.Embed], discord.Embed]] - ] = pages - self.default: Optional[bool] = default + self.description: str | None = description + self.emoji: str | discord.Emoji | discord.PartialEmoji = emoji + self.pages: (list[str] | list[list[discord.Embed] | discord.Embed]) = pages + self.default: bool | None = default self.show_disabled = show_disabled self.show_indicator = show_indicator self.author_check = author_check @@ -307,7 +302,7 @@ def __init__( self.loop_pages = loop_pages self.custom_view: discord.ui.View = custom_view self.timeout: float = timeout - self.custom_buttons: List = custom_buttons + self.custom_buttons: list = custom_buttons self.trigger_on_display = trigger_on_display @@ -375,12 +370,12 @@ class Paginator(discord.ui.View): def __init__( self, - pages: Union[ - List[PageGroup], - List[Page], - List[str], - List[Union[List[discord.Embed], discord.Embed]], - ], + pages: ( + list[PageGroup] + | list[Page] + | list[str] + | list[list[discord.Embed] | discord.Embed] + ), show_disabled: bool = True, show_indicator=True, show_menu=False, @@ -390,24 +385,24 @@ def __init__( use_default_buttons=True, default_button_row: int = 0, loop_pages=False, - custom_view: Optional[discord.ui.View] = None, - timeout: Optional[float] = 180.0, - custom_buttons: Optional[List[PaginatorButton]] = None, - trigger_on_display: Optional[bool] = None, + custom_view: discord.ui.View | None = None, + timeout: float | None = 180.0, + custom_buttons: list[PaginatorButton] | None = None, + trigger_on_display: bool | None = None, ) -> None: super().__init__(timeout=timeout) self.timeout: float = timeout - self.pages: Union[ - List[PageGroup], - List[str], - List[Page], - List[Union[List[discord.Embed], discord.Embed]], - ] = pages + self.pages: ( + list[PageGroup] + | list[str] + | list[Page] + | list[list[discord.Embed] | discord.Embed] + ) = pages self.current_page = 0 - self.menu: Optional[PaginatorMenu] = None + self.menu: PaginatorMenu | None = None self.show_menu = show_menu self.menu_placeholder = menu_placeholder - self.page_groups: Optional[List[PageGroup]] = None + self.page_groups: list[PageGroup] | None = None self.default_page_group: int = 0 if all(isinstance(pg, PageGroup) for pg in pages): @@ -418,13 +413,13 @@ def __init__( if pg.default: self.default_page_group = self.page_groups.index(pg) break - self.pages: List[Page] = self.get_page_group_content( + self.pages: list[Page] = self.get_page_group_content( self.page_groups[self.default_page_group] ) self.page_count = max(len(self.pages) - 1, 0) self.buttons = {} - self.custom_buttons: List = custom_buttons + self.custom_buttons: list = custom_buttons self.show_disabled = show_disabled self.show_indicator = show_indicator self.disable_on_timeout = disable_on_timeout @@ -433,7 +428,7 @@ def __init__( self.loop_pages = loop_pages self.custom_view: discord.ui.View = custom_view self.trigger_on_display = trigger_on_display - self.message: Union[discord.Message, discord.WebhookMessage, None] = None + self.message: discord.Message | discord.WebhookMessage | None = None if self.custom_buttons and not self.use_default_buttons: for button in custom_buttons: @@ -449,28 +444,27 @@ def __init__( async def update( self, - pages: Optional[ - Union[ - List[PageGroup], - List[Page], - List[str], - List[Union[List[discord.Embed], discord.Embed]], - ] - ] = None, - show_disabled: Optional[bool] = None, - show_indicator: Optional[bool] = None, - show_menu: Optional[bool] = None, - author_check: Optional[bool] = None, - menu_placeholder: Optional[str] = None, - disable_on_timeout: Optional[bool] = None, - use_default_buttons: Optional[bool] = None, - default_button_row: Optional[int] = None, - loop_pages: Optional[bool] = None, - custom_view: Optional[discord.ui.View] = None, - timeout: Optional[float] = None, - custom_buttons: Optional[List[PaginatorButton]] = None, - trigger_on_display: Optional[bool] = None, - interaction: Optional[discord.Interaction] = None, + pages: None + | ( + list[PageGroup] + | list[Page] + | list[str] + | list[list[discord.Embed] | discord.Embed] + ) = None, + show_disabled: bool | None = None, + show_indicator: bool | None = None, + show_menu: bool | None = None, + author_check: bool | None = None, + menu_placeholder: str | None = None, + disable_on_timeout: bool | None = None, + use_default_buttons: bool | None = None, + default_button_row: int | None = None, + loop_pages: bool | None = None, + custom_view: discord.ui.View | None = None, + timeout: float | None = None, + custom_buttons: list[PaginatorButton] | None = None, + trigger_on_display: bool | None = None, + interaction: discord.Interaction | None = None, ): """Updates the existing :class:`Paginator` instance with the provided options. @@ -514,14 +508,12 @@ async def update( """ # Update pages and reset current_page to 0 (default) - self.pages: Union[ - List[PageGroup], - List[str], - List[Page], - List[Union[List[discord.Embed], discord.Embed]], - ] = ( - pages if pages is not None else self.pages - ) + self.pages: ( + list[PageGroup] + | list[str] + | list[Page] + | list[list[discord.Embed] | discord.Embed] + ) = (pages if pages is not None else self.pages) self.show_menu = show_menu if show_menu is not None else self.show_menu if pages is not None and all(isinstance(pg, PageGroup) for pg in pages): self.page_groups = self.pages if self.show_menu else None @@ -531,7 +523,7 @@ async def update( if pg.default: self.default_page_group = self.page_groups.index(pg) break - self.pages: List[Page] = self.get_page_group_content( + self.pages: list[Page] = self.get_page_group_content( self.page_groups[self.default_page_group] ) self.page_count = max(len(self.pages) - 1, 0) @@ -597,9 +589,7 @@ async def on_timeout(self) -> None: async def disable( self, include_custom: bool = False, - page: Optional[ - Union[str, Page, Union[List[discord.Embed], discord.Embed]] - ] = None, + page: None | (str | Page | list[discord.Embed] | discord.Embed) = None, ) -> None: """Stops the paginator, disabling all of its components. @@ -630,9 +620,7 @@ async def disable( async def cancel( self, include_custom: bool = False, - page: Optional[ - Union[str, Page, Union[List[discord.Embed], discord.Embed]] - ] = None, + page: None | (str | Page | list[discord.Embed] | discord.Embed) = None, ) -> None: """Cancels the paginator, removing all of its components from the message. @@ -662,7 +650,7 @@ async def cancel( await self.message.edit(view=self) async def goto_page( - self, page_number: int = 0, *, interaction: Optional[discord.Interaction] = None + self, page_number: int = 0, *, interaction: discord.Interaction | None = None ) -> None: """Updates the paginator message to show the specified page number. @@ -778,11 +766,15 @@ def add_button(self, button: PaginatorButton): self.buttons[button.button_type] = { "object": discord.ui.Button( style=button.style, - label=button.label - if button.label or button.emoji - else button.button_type.capitalize() - if button.button_type != "page_indicator" - else f"{self.current_page + 1}/{self.page_count + 1}", + label=( + button.label + if button.label or button.emoji + else ( + button.button_type.capitalize() + if button.button_type != "page_indicator" + else f"{self.current_page + 1}/{self.page_count + 1}" + ) + ), disabled=button.disabled, custom_id=button.custom_id, emoji=button.emoji, @@ -790,9 +782,11 @@ def add_button(self, button: PaginatorButton): ), "label": button.label, "loop_label": button.loop_label, - "hidden": button.disabled - if button.button_type != "page_indicator" - else not self.show_indicator, + "hidden": ( + button.disabled + if button.button_type != "page_indicator" + else not self.show_indicator + ), } self.buttons[button.button_type]["object"].callback = button.callback button.paginator = self @@ -805,7 +799,7 @@ def remove_button(self, button_type: str): ) self.buttons.pop(button_type) - def update_buttons(self) -> Dict: + def update_buttons(self) -> dict: """Updates the display state of the buttons (disabled/hidden) Returns @@ -879,13 +873,13 @@ def update_custom_view(self, custom_view: discord.ui.View): for item in custom_view.children: self.add_item(item) - def get_page_group_content(self, page_group: PageGroup) -> List[Page]: + def get_page_group_content(self, page_group: PageGroup) -> list[Page]: """Returns a converted list of `Page` objects for the given page group based on the content of its pages.""" return [self.get_page_content(page) for page in page_group.pages] @staticmethod def get_page_content( - page: Union[Page, str, discord.Embed, List[discord.Embed]] + page: Page | str | discord.Embed | list[discord.Embed], ) -> Page: """Converts a page into a :class:`Page` object based on its content.""" if isinstance(page, Page): @@ -905,12 +899,11 @@ def get_page_content( raise TypeError("All list items must be embeds or files.") else: raise TypeError( - "Page content must be a Page object, string, an embed, a list of embeds, a file, or a list of files." + "Page content must be a Page object, string, an embed, a list of" + " embeds, a file, or a list of files." ) - async def page_action( - self, interaction: Optional[discord.Interaction] = None - ) -> None: + async def page_action(self, interaction: discord.Interaction | None = None) -> None: """Triggers the callback associated with the current page, if any. Parameters @@ -926,14 +919,13 @@ async def page_action( async def send( self, ctx: Context, - target: Optional[discord.abc.Messageable] = None, - target_message: Optional[str] = None, - reference: Optional[ - Union[discord.Message, discord.MessageReference, discord.PartialMessage] - ] = None, - allowed_mentions: Optional[discord.AllowedMentions] = None, - mention_author: Optional[bool] = None, - delete_after: Optional[float] = None, + target: discord.abc.Messageable | None = None, + target_message: str | None = None, + reference: None + | (discord.Message | discord.MessageReference | discord.PartialMessage) = None, + allowed_mentions: discord.AllowedMentions | None = None, + mention_author: bool | None = None, + delete_after: float | None = None, ) -> discord.Message: """Sends a message with the paginated items. @@ -979,7 +971,8 @@ async def send( (discord.Message, discord.MessageReference, discord.PartialMessage), ): raise TypeError( - f"expected Message, MessageReference, or PartialMessage not {reference.__class__!r}" + "expected Message, MessageReference, or PartialMessage not" + f" {reference.__class__!r}" ) if allowed_mentions is not None and not isinstance( @@ -1027,10 +1020,10 @@ async def send( async def edit( self, message: discord.Message, - suppress: Optional[bool] = None, - allowed_mentions: Optional[discord.AllowedMentions] = None, - delete_after: Optional[float] = None, - ) -> Optional[discord.Message]: + suppress: bool | None = None, + allowed_mentions: discord.AllowedMentions | None = None, + delete_after: float | None = None, + ) -> discord.Message | None: """Edits an existing message to replace it with the paginator contents. .. note:: @@ -1066,7 +1059,7 @@ async def edit( self.update_buttons() - page: Union[Page, str, discord.Embed, List[discord.Embed]] = self.pages[ + page: Page | str | discord.Embed | list[discord.Embed] = self.pages[ self.current_page ] page_content: Page = self.get_page_content(page) @@ -1094,11 +1087,11 @@ async def edit( async def respond( self, - interaction: Union[discord.Interaction, BridgeContext], + interaction: discord.Interaction | BridgeContext, ephemeral: bool = False, - target: Optional[discord.abc.Messageable] = None, + target: discord.abc.Messageable | None = None, target_message: str = "Paginator sent!", - ) -> Union[discord.Message, discord.WebhookMessage]: + ) -> discord.Message | discord.WebhookMessage: """Sends an interaction response or followup with the paginated items. Parameters @@ -1137,12 +1130,13 @@ async def respond( if ephemeral and (self.timeout >= 900 or self.timeout is None): raise ValueError( - "paginator responses cannot be ephemeral if the paginator timeout is 15 minutes or greater" + "paginator responses cannot be ephemeral if the paginator timeout is 15" + " minutes or greater" ) self.update_buttons() - page: Union[Page, str, discord.Embed, List[discord.Embed]] = self.pages[ + page: Page | str | discord.Embed | list[discord.Embed] = self.pages[ self.current_page ] page_content: Page = self.get_page_content(page) @@ -1226,12 +1220,12 @@ class PaginatorMenu(discord.ui.Select): def __init__( self, - page_groups: List[PageGroup], - placeholder: Optional[str] = None, - custom_id: Optional[str] = None, + page_groups: list[PageGroup], + placeholder: str | None = None, + custom_id: str | None = None, ): self.page_groups = page_groups - self.paginator: Optional[Paginator] = None + self.paginator: Paginator | None = None opts = [ discord.SelectOption( label=page_group.label, diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index df5318b9..400786d8 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -227,7 +227,7 @@ def __get__(self, obj: T, objtype: type[T]) -> Loop[LF]: @property def seconds(self) -> float | None: - """Optional[:class:`float`]: Read-only value for the number of seconds + """Read-only value for the number of seconds between each iteration. ``None`` if an explicit ``time`` value was passed instead. .. versionadded:: 2.0 @@ -237,7 +237,7 @@ def seconds(self) -> float | None: @property def minutes(self) -> float | None: - """Optional[:class:`float`]: Read-only value for the number of minutes + """Read-only value for the number of minutes between each iteration. ``None`` if an explicit ``time`` value was passed instead. .. versionadded:: 2.0 @@ -247,7 +247,7 @@ def minutes(self) -> float | None: @property def hours(self) -> float | None: - """Optional[:class:`float`]: Read-only value for the number of hours + """Read-only value for the number of hours between each iteration. ``None`` if an explicit ``time`` value was passed instead. .. versionadded:: 2.0 @@ -257,7 +257,7 @@ def hours(self) -> float | None: @property def time(self) -> list[datetime.time] | None: - """Optional[List[:class:`datetime.time`]]: Read-only list for the exact times this loop runs at. + """Read-only list for the exact times this loop runs at. ``None`` if relative times were passed instead. .. versionadded:: 2.0 @@ -267,12 +267,12 @@ def time(self) -> list[datetime.time] | None: @property def current_loop(self) -> int: - """:class:`int`: The current iteration of the loop.""" + """The current iteration of the loop.""" return self._current_loop @property def next_iteration(self) -> datetime.datetime | None: - """Optional[:class:`datetime.datetime`]: When the next iteration of the loop will occur. + """When the next iteration of the loop will occur. .. versionadded:: 1.3 """ @@ -428,7 +428,7 @@ def clear_exception_types(self) -> None: This operation obviously cannot be undone! """ - self._valid_exception = tuple() + self._valid_exception = () def remove_exception_type(self, *exceptions: type[BaseException]) -> bool: r"""Removes exception types from being handled during the reconnect logic. @@ -450,7 +450,7 @@ def remove_exception_type(self, *exceptions: type[BaseException]) -> bool: return len(self._valid_exception) == old_length - len(exceptions) def get_task(self) -> asyncio.Task[None] | None: - """Optional[:class:`asyncio.Task`]: Fetches the internal task or ``None`` if there isn't one running.""" + """Fetches the internal task or ``None`` if there isn't one running.""" return self._task if self._task is not MISSING else None def is_being_cancelled(self) -> bool: @@ -458,14 +458,14 @@ def is_being_cancelled(self) -> bool: return self._is_being_cancelled def failed(self) -> bool: - """:class:`bool`: Whether the internal task has failed. + """Whether the internal task has failed. .. versionadded:: 1.2 """ return self._has_failed def is_running(self) -> bool: - """:class:`bool`: Check if the task is currently running. + """Check if the task is currently running. .. versionadded:: 1.4 """ @@ -631,7 +631,8 @@ def _get_time_parameter( return [inner] if not isinstance(time, Sequence): raise TypeError( - f"Expected datetime.time or a sequence of datetime.time for ``time``, received {type(time)!r} instead." + "Expected datetime.time or a sequence of datetime.time for ``time``," + f" received {type(time)!r} instead." ) if not time: raise ValueError("time parameter must not be an empty sequence.") @@ -640,8 +641,8 @@ def _get_time_parameter( for index, t in enumerate(time): if not isinstance(t, dt): raise TypeError( - f"Expected a sequence of {dt!r} for ``time``, received {type(t).__name__!r}" - f" at index {index} instead." + f"Expected a sequence of {dt!r} for ``time``, received" + f" {type(t).__name__!r} at index {index} instead." ) ret.append(t if t.tzinfo is not None else t.replace(tzinfo=utc)) diff --git a/discord/flags.py b/discord/flags.py index 2bf94b18..ec44aaaf 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -379,7 +379,7 @@ def ephemeral(self): def loading(self): """:class:`bool`: Returns ``True`` if the source message is deferred. - The user sees a 'thinking' state + The user sees a 'thinking' state. .. versionadded:: 2.0 """ @@ -393,6 +393,17 @@ def failed_to_mention_some_roles_in_thread(self): """ return 256 + @flag_value + def suppress_notifications(self): + """:class:`bool`: Returns ``True`` if the source message does not trigger push and desktop notifications. + + Users will still receive mentions. + + .. versionadded:: 2.4 + """ + + return 4096 + @fill_with_flags() class PublicUserFlags(BaseFlags): @@ -536,6 +547,14 @@ def bot_http_interactions(self): """ return UserFlags.bot_http_interactions.value + @flag_value + def active_developer(self): + """:class:`bool`: Returns ``True`` if the user is an Active Developer. + + .. versionadded:: 2.3 + """ + return UserFlags.active_developer.value + def all(self) -> list[UserFlags]: """List[:class:`UserFlags`]: Returns all public flags the user has.""" return [ @@ -670,6 +689,7 @@ def members(self): - :func:`on_member_join` - :func:`on_member_remove` + - :func:`on_raw_member_remove` - :func:`on_member_update` - :func:`on_user_update` @@ -1047,7 +1067,7 @@ def message_content(self): .. note:: - As of September 2022 requires opting in explicitly via the Developer Portal to receive the actual content + As of September 2022 using this intent requires opting in explicitly via the Developer Portal to receive the actual content of the guild messages. This intent is privileged, meaning that bots in over 100 guilds that require this intent would need to request this intent on the Developer Portal. See https://support-dev.discord.com/hc/en-us/articles/4404772028055 for more information. @@ -1374,6 +1394,15 @@ def app_commands_badge(self): """ return 1 << 23 + @flag_value + def active(self): + """:class:`bool`: Returns ``True`` if the app is considered active. + Applications are considered active if they have had any command executions in the past 30 days. + + .. versionadded:: 2.3 + """ + return 1 << 24 + @fill_with_flags() class ChannelFlags(BaseFlags): @@ -1424,3 +1453,12 @@ class ChannelFlags(BaseFlags): def pinned(self): """:class:`bool`: Returns ``True`` if the thread is pinned to the top of its parent 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.2 + """ + return 1 << 4 diff --git a/discord/gateway.py b/discord/gateway.py index 15aa2040..bd13f22e 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -22,6 +22,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations import asyncio import concurrent.futures @@ -139,7 +140,10 @@ def run(self): while not self._stop_ev.wait(self.interval): if self._last_recv + self.heartbeat_timeout < time.perf_counter(): _log.warning( - "Shard ID %s has stopped responding to the gateway. Closing and restarting.", + ( + "Shard ID %s has stopped responding to the gateway. Closing and" + " restarting." + ), self.shard_id, ) coro = self.ws.close(4000) @@ -174,7 +178,10 @@ def run(self): msg = self.block_msg else: stack = "".join(traceback.format_stack(frame)) - msg = f"{self.block_msg}\nLoop thread traceback (most recent call last):\n{stack}" + msg = ( + f"{self.block_msg}\nLoop thread traceback (most recent" + f" call last):\n{stack}" + ) _log.warning(msg, self.shard_id, total) except Exception: @@ -572,8 +579,8 @@ async def received_message(self, msg, /): del self._dispatch_listeners[index] @property - def latency(self): - """:class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.""" + def latency(self) -> float: + """Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.""" heartbeat = self._keep_alive return float("inf") if heartbeat is None else heartbeat.latency @@ -885,16 +892,16 @@ async def initial_connection(self, data): state.voice_port = data["port"] state.endpoint_ip = data["ip"] - packet = bytearray(70) + packet = bytearray(74) struct.pack_into(">H", packet, 0, 1) # 1 = Send struct.pack_into(">H", packet, 2, 70) # 70 = Length struct.pack_into(">I", packet, 4, state.ssrc) state.socket.sendto(packet, (state.endpoint_ip, state.voice_port)) - recv = await self.loop.sock_recv(state.socket, 70) + recv = await self.loop.sock_recv(state.socket, 74) _log.debug("received packet in initial_connection: %s", recv) - # the ip is ascii starting at the 4th byte and ending at the first null - ip_start = 4 + # the ip is ascii starting at the 8th byte and ending at the first null + ip_start = 8 ip_end = recv.index(0, ip_start) state.ip = recv[ip_start:ip_end].decode("ascii") @@ -912,14 +919,14 @@ async def initial_connection(self, data): _log.info("selected the voice protocol for use (%s)", mode) @property - def latency(self): - """:class:`float`: Latency between a HEARTBEAT and its HEARTBEAT_ACK in seconds.""" + def latency(self) -> float: + """Latency between a HEARTBEAT and its HEARTBEAT_ACK in seconds.""" heartbeat = self._keep_alive return float("inf") if heartbeat is None else heartbeat.latency @property - def average_latency(self): - """:class:`list`: Average of last 20 HEARTBEAT latencies.""" + def average_latency(self) -> list[float] | float: + """Average of last 20 HEARTBEAT latencies.""" heartbeat = self._keep_alive if heartbeat is None or not heartbeat.recent_ack_latencies: return float("inf") diff --git a/discord/guild.py b/discord/guild.py index 6a3a51e8..067f71ed 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -208,17 +208,20 @@ class Guild(Hashable): - ``ANIMATED_BANNER``: Guild can upload an animated banner. - ``ANIMATED_ICON``: Guild can upload an animated icon. + - ``APPLICATION_COMMAND_PERMISSIONS_V2``: Guild is using the old command permissions behavior. - ``AUTO_MODERATION``: Guild has enabled the auto moderation system. - ``BANNER``: Guild can upload and use a banner. (i.e. :attr:`.banner`) - ``CHANNEL_BANNER``: Guild can upload and use a channel banners. - ``COMMERCE``: Guild can sell things using store channels, which have now been removed. - ``COMMUNITY``: Guild is a community server. + - ``DEVELOPER_SUPPORT_SERVER``: Guild has been set as a support server on the App Directory. - ``DISCOVERABLE``: Guild shows up in Server Discovery. + - ``FEATURABLE``: Guild can be featured in the Server Directory. - ``HAS_DIRECTORY_ENTRY``: Unknown. - ``HUB``: Hubs contain a directory channel that let you find school-related, student-run servers for your school or university. - ``INTERNAL_EMPLOYEE_ONLY``: Indicates that only users with the staff badge can join the guild. - - ``INVITE_SPLASH``: Guild's invite page can have a special splash. - ``INVITES_DISABLED``: Guild Invites are disabled. + - ``INVITE_SPLASH``: Guild's invite page can have a special splash. - ``LINKED_TO_HUB``: 'Guild is linked to a hub. - ``MEMBER_PROFILES``: Unknown. - ``MEMBER_VERIFICATION_GATE_ENABLED``: Guild has Membership Screening enabled. @@ -230,12 +233,13 @@ class Guild(Hashable): - ``PARTNERED``: Guild is a partnered server. - ``PREMIUM_TIER_3_OVERRIDE``: Forces the server to server boosting level 3 (specifically created by Discord Staff Member "Jethro" for their personal server). - ``PREVIEW_ENABLED``: Guild can be viewed before being accepted via Membership Screening. - - ``PRIVATE_THREADS``: Guild has access to create private threads. - ``ROLE_ICONS``: Guild can set an image or emoji as a role icon. - ``ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE``: Role subscriptions are available for purchasing. - ``ROLE_SUBSCRIPTIONS_ENABLED``: Guild is able to view and manage role subscriptions. + - ``SEVEN_DAY_THREAD_ARCHIVE``: Users can set the thread archive time to 7 days. - ``TEXT_IN_VOICE_ENABLED``: Guild has a chat button inside voice channels that opens a dedicated text channel in a sidebar similar to thread view. - ``THREADS_ENABLED_TESTING``: Used by bot developers to test their bots with threads in guilds with 5 or fewer members and a bot. Also gives the premium thread features. + - ``THREE_DAY_THREAD_ARCHIVE``: Users can set the thread archive time to 3 days. - ``TICKETED_EVENTS_ENABLED``: Guild has enabled ticketed events. - ``VANITY_URL``: Guild can have a vanity invite URL (e.g. discord.gg/discord-api). - ``VERIFIED``: Guild is a verified server. @@ -588,7 +592,7 @@ def _sync(self, data: GuildPayload) -> None: except KeyError: pass - empty_tuple = tuple() + empty_tuple = () for presence in data.get("presences", []): user_id = int(presence["user"]["id"]) member = self.get_member(user_id) @@ -609,12 +613,12 @@ def _sync(self, data: GuildPayload) -> None: @property def channels(self) -> list[GuildChannel]: - """List[:class:`abc.GuildChannel`]: A list of channels that belong to this guild.""" + """A list of channels that belong to this guild.""" return list(self._channels.values()) @property def threads(self) -> list[Thread]: - """List[:class:`Thread`]: A list of threads that you have permission to view. + """A list of threads that you have permission to view. .. versionadded:: 2.0 """ @@ -622,7 +626,7 @@ def threads(self) -> list[Thread]: @property def jump_url(self) -> str: - """:class:`str`: Returns a URL that allows the client to jump to the guild. + """Returns a URL that allows the client to jump to the guild. .. versionadded:: 2.0 """ @@ -630,7 +634,7 @@ def jump_url(self) -> str: @property def large(self) -> bool: - """:class:`bool`: Indicates if the guild is a 'large' guild. + """Indicates if the guild is a 'large' guild. A large guild is defined as having more than ``large_threshold`` count members, which for this library is set to the maximum of 250. @@ -644,7 +648,7 @@ def large(self) -> bool: @property def voice_channels(self) -> list[VoiceChannel]: - """List[:class:`VoiceChannel`]: A list of voice channels that belong to this guild. + """A list of voice channels that belong to this guild. This is sorted by the position and are in UI order from top to bottom. """ @@ -654,7 +658,7 @@ def voice_channels(self) -> list[VoiceChannel]: @property def stage_channels(self) -> list[StageChannel]: - """List[:class:`StageChannel`]: A list of stage channels that belong to this guild. + """A list of stage channels that belong to this guild. .. versionadded:: 1.7 @@ -666,7 +670,7 @@ def stage_channels(self) -> list[StageChannel]: @property def forum_channels(self) -> list[ForumChannel]: - """List[:class:`ForumChannel`]: A list of forum channels that belong to this guild. + """A list of forum channels that belong to this guild. .. versionadded:: 2.0 @@ -678,7 +682,7 @@ def forum_channels(self) -> list[ForumChannel]: @property def me(self) -> Member: - """:class:`Member`: Similar to :attr:`Client.user` except an instance of :class:`Member`. + """Similar to :attr:`Client.user` except an instance of :class:`Member`. This is essentially used to get the member version of yourself. """ self_id = self._state.user.id @@ -687,12 +691,12 @@ def me(self) -> Member: @property def voice_client(self) -> VoiceProtocol | None: - """Optional[:class:`VoiceProtocol`]: Returns the :class:`VoiceProtocol` associated with this guild, if any.""" + """Returns the :class:`VoiceProtocol` associated with this guild, if any.""" return self._state._get_voice_client(self.id) @property def text_channels(self) -> list[TextChannel]: - """List[:class:`TextChannel`]: A list of text channels that belong to this guild. + """A list of text channels that belong to this guild. This is sorted by the position and are in UI order from top to bottom. """ @@ -702,7 +706,7 @@ def text_channels(self) -> list[TextChannel]: @property def categories(self) -> list[CategoryChannel]: - """List[:class:`CategoryChannel`]: A list of categories that belong to this guild. + """A list of categories that belong to this guild. This is sorted by the position and are in UI order from top to bottom. """ @@ -806,7 +810,7 @@ def get_thread(self, thread_id: int, /) -> Thread | None: @property def system_channel(self) -> TextChannel | None: - """Optional[:class:`TextChannel`]: Returns the guild's channel used for system messages. + """Returns the guild's channel used for system messages. If no channel is set, then this returns ``None``. """ @@ -815,12 +819,12 @@ def system_channel(self) -> TextChannel | None: @property def system_channel_flags(self) -> SystemChannelFlags: - """:class:`SystemChannelFlags`: Returns the guild's system channel settings.""" + """Returns the guild's system channel settings.""" return SystemChannelFlags._from_value(self._system_channel_flags) @property def rules_channel(self) -> TextChannel | None: - """Optional[:class:`TextChannel`]: Return's the guild's channel used for the rules. + """Return's the guild's channel used for the rules. The guild must be a Community guild. If no channel is set, then this returns ``None``. @@ -832,7 +836,7 @@ def rules_channel(self) -> TextChannel | None: @property def public_updates_channel(self) -> TextChannel | None: - """Optional[:class:`TextChannel`]: Return's the guild's channel where admins and + """Return's the guild's channel where admins and moderators of the guilds receive notices from Discord. The guild must be a Community guild. @@ -845,13 +849,13 @@ def public_updates_channel(self) -> TextChannel | None: @property def emoji_limit(self) -> int: - """:class:`int`: The maximum number of emoji slots this guild has.""" + """The maximum number of emoji slots this guild has.""" more_emoji = 200 if "MORE_EMOJI" in self.features else 50 return max(more_emoji, self._PREMIUM_GUILD_LIMITS[self.premium_tier].emoji) @property def sticker_limit(self) -> int: - """:class:`int`: The maximum number of sticker slots this guild has. + """The maximum number of sticker slots this guild has. .. versionadded:: 2.0 """ @@ -862,7 +866,7 @@ def sticker_limit(self) -> int: @property def bitrate_limit(self) -> float: - """:class:`float`: The maximum bitrate for voice channels this guild can have.""" + """The maximum bitrate for voice channels this guild can have.""" vip_guild = ( self._PREMIUM_GUILD_LIMITS[1].bitrate if "VIP_REGIONS" in self.features @@ -872,12 +876,12 @@ def bitrate_limit(self) -> float: @property def filesize_limit(self) -> int: - """:class:`int`: The maximum number of bytes files can have when uploaded to this guild.""" + """The maximum number of bytes files can have when uploaded to this guild.""" return self._PREMIUM_GUILD_LIMITS[self.premium_tier].filesize @property def members(self) -> list[Member]: - """List[:class:`Member`]: A list of members that belong to this guild.""" + """A list of members that belong to this guild.""" return list(self._members.values()) def get_member(self, user_id: int, /) -> Member | None: @@ -897,12 +901,12 @@ def get_member(self, user_id: int, /) -> Member | None: @property def premium_subscribers(self) -> list[Member]: - """List[:class:`Member`]: A list of members who have "boosted" this guild.""" + """A list of members who have "boosted" this guild.""" return [member for member in self.members if member.premium_since is not None] @property def roles(self) -> list[Role]: - """List[:class:`Role`]: Returns a :class:`list` of the guild's roles in hierarchy order. + """Returns a :class:`list` of the guild's roles in hierarchy order. The first element of this list will be the lowest role in the hierarchy. @@ -926,13 +930,13 @@ def get_role(self, role_id: int, /) -> Role | None: @property def default_role(self) -> Role: - """:class:`Role`: Gets the @everyone role that all members have by default.""" + """Gets the @everyone role that all members have by default.""" # The @everyone role is *always* given return self.get_role(self.id) # type: ignore @property def premium_subscriber_role(self) -> Role | None: - """Optional[:class:`Role`]: Gets the premium subscriber role, AKA "boost" role, in this guild. + """Gets the premium subscriber role, AKA "boost" role, in this guild. .. versionadded:: 1.6 """ @@ -943,7 +947,7 @@ def premium_subscriber_role(self) -> Role | None: @property def self_role(self) -> Role | None: - """Optional[:class:`Role`]: Gets the role associated with this client's user, if any. + """Gets the role associated with this client's user, if any. .. versionadded:: 1.6 """ @@ -956,7 +960,7 @@ def self_role(self) -> Role | None: @property def stage_instances(self) -> list[StageInstance]: - """List[:class:`StageInstance`]: Returns a :class:`list` of the guild's stage instances that + """Returns a :class:`list` of the guild's stage instances that are currently running. .. versionadded:: 2.0 @@ -982,19 +986,19 @@ def get_stage_instance(self, stage_instance_id: int, /) -> StageInstance | None: @property def owner(self) -> Member | None: - """Optional[:class:`Member`]: The member that owns the guild.""" + """The member that owns the guild.""" return self.get_member(self.owner_id) # type: ignore @property def icon(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the guild's icon asset, if available.""" + """Returns the guild's icon asset, if available.""" if self._icon is None: return None return Asset._from_guild_icon(self._state, self.id, self._icon) @property def banner(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the guild's banner asset, if available.""" + """Returns the guild's banner asset, if available.""" if self._banner is None: return None return Asset._from_guild_image( @@ -1003,7 +1007,7 @@ def banner(self) -> Asset | None: @property def splash(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available.""" + """Returns the guild's invite splash asset, if available.""" if self._splash is None: return None return Asset._from_guild_image( @@ -1012,7 +1016,7 @@ def splash(self) -> Asset | None: @property def discovery_splash(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the guild's discovery splash asset, if available.""" + """Returns the guild's discovery splash asset, if available.""" if self._discovery_splash is None: return None return Asset._from_guild_image( @@ -1021,7 +1025,7 @@ def discovery_splash(self) -> Asset | None: @property def member_count(self) -> int: - """:class:`int`: Returns the true member count regardless of it being loaded fully or not. + """Returns the true member count regardless of it being loaded fully or not. .. warning:: @@ -1032,7 +1036,7 @@ def member_count(self) -> int: @property def chunked(self) -> bool: - """:class:`bool`: Returns a boolean indicating if the guild is "chunked". + """Returns a boolean indicating if the guild is "chunked". A chunked guild means that :attr:`member_count` is equal to the number of members stored in the internal :attr:`members` cache. @@ -1047,7 +1051,7 @@ def chunked(self) -> bool: @property def shard_id(self) -> int: - """:class:`int`: Returns the shard ID for this guild if applicable.""" + """Returns the shard ID for this guild if applicable.""" count = self._state.shard_count if count is None: return 0 @@ -1055,12 +1059,12 @@ def shard_id(self) -> int: @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the guild's creation time in UTC.""" + """Returns the guild's creation time in UTC.""" return utils.snowflake_time(self.id) @property def invites_disabled(self) -> bool: - """:class:`bool`: Returns a boolean indicating if the guild invites are disabled.""" + """Returns a boolean indicating if the guild invites are disabled.""" return "INVITES_DISABLED" in self.features def get_member_named(self, name: str, /) -> Member | None: @@ -1136,9 +1140,11 @@ def _create_channel( "allow": allow.value, "deny": deny.value, "id": target.id, - "type": abc._Overwrites.ROLE - if isinstance(target, Role) - else abc._Overwrites.MEMBER, + "type": ( + abc._Overwrites.ROLE + if isinstance(target, Role) + else abc._Overwrites.MEMBER + ), } perms.append(payload) @@ -1885,7 +1891,8 @@ async def edit( features.append("COMMUNITY") else: raise InvalidArgument( - "community field requires both rules_channel and public_updates_channel fields to be provided" + "community field requires both rules_channel and" + " public_updates_channel fields to be provided" ) else: if "COMMUNITY" in features: @@ -2265,7 +2272,8 @@ async def prune_members( if not isinstance(days, int): raise InvalidArgument( - f"Expected int for ``days``, received {days.__class__.__name__} instead." + "Expected int for ``days``, received" + f" {days.__class__.__name__} instead." ) role_ids = [str(role.id) for role in roles] if roles else [] @@ -2361,7 +2369,8 @@ async def estimate_pruned_members( if not isinstance(days, int): raise InvalidArgument( - f"Expected int for ``days``, received {days.__class__.__name__} instead." + "Expected int for ``days``, received" + f" {days.__class__.__name__} instead." ) role_ids = [str(role.id) for role in roles] if roles else [] @@ -2986,7 +2995,6 @@ async def edit_role_positions( role_positions: list[dict[str, Any]] = [] for role, position in positions.items(): - payload = {"id": role.id, "position": position} role_positions.append(payload) @@ -3644,6 +3652,7 @@ async def create_scheduled_event( location: str | int | VoiceChannel | StageChannel | ScheduledEventLocation, privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, reason: str | None = None, + image: bytes = MISSING, ) -> ScheduledEvent | None: """|coro| Creates a scheduled event. @@ -3666,6 +3675,8 @@ async def create_scheduled_event( so there is no need to change this parameter. reason: Optional[:class:`str`] The reason to show in the audit log. + image: Optional[:class:`bytes`] + The cover image of the scheduled event Returns ------- @@ -3703,6 +3714,9 @@ async def create_scheduled_event( if end_time is not MISSING: payload["scheduled_end_time"] = end_time.isoformat() + if image is not MISSING: + payload["image"] = utils._bytes_to_base64_data(image) + data = await self._state.http.create_scheduled_event( guild_id=self.id, reason=reason, **payload ) @@ -3714,7 +3728,7 @@ async def create_scheduled_event( @property def scheduled_events(self) -> list[ScheduledEvent]: - """List[:class:`.ScheduledEvent`]: A list of scheduled events in this guild.""" + """A list of scheduled events in this guild.""" return list(self._scheduled_events.values()) async def fetch_auto_moderation_rules(self) -> list[AutoModRule]: @@ -3821,5 +3835,7 @@ async def create_auto_moderation_rule( if exempt_channels: payload["exempt_channels"] = [c.id for c in exempt_channels] - data = await self._state.http.create_auto_moderation_rule(self.id, payload) - return AutoModRule(state=self._state, data=data, reason=reason) + data = await self._state.http.create_auto_moderation_rule( + self.id, payload, reason=reason + ) + return AutoModRule(state=self._state, data=data) diff --git a/discord/http.py b/discord/http.py index 3e246004..d70380cd 100644 --- a/discord/http.py +++ b/discord/http.py @@ -56,6 +56,7 @@ from .file import File from .types import ( appinfo, + application_role_connection, audit_log, automod, channel, @@ -182,7 +183,9 @@ def __init__( self.proxy_auth: aiohttp.BasicAuth | None = proxy_auth self.use_clock: bool = not unsync_clock - user_agent = "DiscordBot (https://github.com/Pycord-Development/pycord {0}) Python/{1[0]}.{1[1]} aiohttp/{2}" + user_agent = ( + "DiscordBot (https://pycord.dev, {0}) Python/{1[0]}.{1[1]} aiohttp/{2}" + ) self.user_agent: str = user_agent.format( __version__, sys.version_info, aiohttp.__version__ ) @@ -299,7 +302,10 @@ async def request( response, use_clock=self.use_clock ) _log.debug( - "A rate limit bucket has been exhausted (bucket: %s, retry: %s).", + ( + "A rate limit bucket has been exhausted (bucket:" + " %s, retry: %s)." + ), bucket, delta, ) @@ -317,7 +323,10 @@ async def request( # Banned by Cloudflare more than likely. raise HTTPException(response, data) - fmt = 'We are being rate limited. Retrying in %.2f seconds. Handled under the bucket "%s"' + fmt = ( + "We are being rate limited. Retrying in %.2f seconds." + ' Handled under the bucket "%s"' + ) # sleep a bit retry_after: float = data["retry_after"] @@ -327,7 +336,10 @@ async def request( is_global = data.get("global", False) if is_global: _log.warning( - "Global rate limit has been hit. Retrying in %.2f seconds.", + ( + "Global rate limit has been hit. Retrying in" + " %.2f seconds." + ), retry_after, ) self._global_over.clear() @@ -896,7 +908,8 @@ def ban( params["delete_message_seconds"] = delete_message_seconds elif delete_message_days: warn_deprecated( - "delete_message_days" "delete_message_seconds", + "delete_message_days", + "delete_message_seconds", "2.2", reference="https://github.com/discord/discord-api-docs/pull/5219", ) @@ -1035,6 +1048,12 @@ def edit_channel( "locked", "invitable", "default_auto_archive_duration", + "flags", + "default_thread_rate_limit_per_user", + "default_reaction_emoji", + "available_tags", + "applied_tags", + "default_sort_order", ) payload = {k: v for k, v in options.items() if k in valid_keys} return self.request(r, reason=reason, json=payload) @@ -1149,6 +1168,7 @@ def start_forum_thread( auto_archive_duration: threads.ThreadArchiveDuration, rate_limit_per_user: int, invitable: bool = True, + applied_tags: SnowflakeList | None = None, reason: str | None = None, embed: embed.Embed | None = None, embeds: list[embed.Embed] | None = None, @@ -1165,6 +1185,9 @@ def start_forum_thread( if content: payload["content"] = content + if applied_tags: + payload["applied_tags"] = applied_tags + if embed: payload["embeds"] = [embed] @@ -2213,6 +2236,7 @@ def create_scheduled_event( "description", "entity_type", "entity_metadata", + "image", ) payload = {k: v for k, v in payload.items() if k in valid_keys} @@ -2582,7 +2606,6 @@ def _edit_webhook_helper( embeds: list[embed.Embed] | None = None, allowed_mentions: message.AllowedMentions | None = None, ): - payload: dict[str, Any] = {} if content: payload["content"] = content @@ -2800,6 +2823,31 @@ def bulk_edit_guild_application_command_permissions( ) return self.request(r, json=payload) + # Application Role Connections + + def get_application_role_connection_metadata_records( + self, + application_id: Snowflake, + ) -> Response[list[application_role_connection.ApplicationRoleConnectionMetadata]]: + r = Route( + "GET", + "/applications/{application_id}/role-connections/metadata", + application_id=application_id, + ) + return self.request(r) + + def update_application_role_connection_metadata_records( + self, + application_id: Snowflake, + payload: list[application_role_connection.ApplicationRoleConnectionMetadata], + ) -> Response[list[application_role_connection.ApplicationRoleConnectionMetadata]]: + r = Route( + "PUT", + "/applications/{application_id}/role-connections/metadata", + application_id=application_id, + ) + return self.request(r, json=payload) + # Misc def application_info(self) -> Response[appinfo.AppInfo]: diff --git a/discord/integrations.py b/discord/integrations.py index 427dd4de..f5d67faa 100644 --- a/discord/integrations.py +++ b/discord/integrations.py @@ -213,12 +213,12 @@ def _from_data(self, data: StreamIntegrationPayload) -> None: @property def expire_behavior(self) -> ExpireBehaviour: - """:class:`ExpireBehaviour`: An alias for :attr:`expire_behaviour`.""" + """An alias for :attr:`expire_behaviour`.""" return self.expire_behaviour @property def role(self) -> Role | None: - """Optional[:class:`Role`]: The role which the integration uses for subscribers.""" + """The role which the integration uses for subscribers.""" return self.guild.get_role(self._role_id) # type: ignore async def edit( diff --git a/discord/interactions.py b/discord/interactions.py index c7721908..462d6133 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -104,7 +104,7 @@ class Interaction: application_id: :class:`int` The application ID that the interaction was for. user: Optional[Union[:class:`User`, :class:`Member`]] - The user or member that sent the interaction. + The user or member that sent the interaction. Will be `None` in PING interactions. message: Optional[:class:`Message`] The message that sent this interaction. token: :class:`str` @@ -208,25 +208,25 @@ def _from_data(self, data: InteractionPayload): @property def client(self) -> Client: - """:class:`Client`: Returns the client that sent the interaction.""" + """Returns the client that sent the interaction.""" return self._state._get_client() @property def guild(self) -> Guild | None: - """Optional[:class:`Guild`]: The guild the interaction was sent from.""" + """The guild the interaction was sent from.""" return self._state and self._state._get_guild(self.guild_id) def is_command(self) -> bool: - """:class:`bool`: Indicates whether the interaction is an application command.""" + """Indicates whether the interaction is an application command.""" return self.type == InteractionType.application_command def is_component(self) -> bool: - """:class:`bool`: Indicates whether the interaction is a message component.""" + """Indicates whether the interaction is a message component.""" return self.type == InteractionType.component @utils.cached_slot_property("_cs_channel") def channel(self) -> InteractionChannel | None: - """Optional[Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]]: The channel the + """The channel the interaction was sent from. Note that due to a Discord limitation, DM channels are not resolved since there is @@ -249,7 +249,7 @@ def channel(self) -> InteractionChannel | None: @property def permissions(self) -> Permissions: - """:class:`Permissions`: The resolved permissions of the member in the channel, including overwrites. + """The resolved permissions of the member in the channel, including overwrites. In a non-guild context where this doesn't apply, an empty permissions object is returned. """ @@ -257,12 +257,12 @@ def permissions(self) -> Permissions: @utils.cached_slot_property("_cs_app_permissions") def app_permissions(self) -> Permissions: - """:class:`Permissions`: The resolved permissions of the application in the channel, including overwrites.""" + """The resolved permissions of the application in the channel, including overwrites.""" return Permissions(self._app_permissions) @utils.cached_slot_property("_cs_response") def response(self) -> InteractionResponse: - """:class:`InteractionResponse`: Returns an object responsible for handling responding to the interaction. + """Returns an object responsible for handling responding to the interaction. A response can only be done once. If secondary messages need to be sent, consider using :attr:`followup` instead. @@ -271,7 +271,7 @@ def response(self) -> InteractionResponse: @utils.cached_slot_property("_cs_followup") def followup(self) -> Webhook: - """:class:`Webhook`: Returns the followup webhook for followup interactions.""" + """Returns the followup webhook for followup interactions.""" payload = { "id": self.application_id, "type": 3, @@ -325,7 +325,7 @@ async def original_response(self) -> InteractionMessage: self._original_response = message return message - @utils.deprecated("Interaction.original_response", "2.1") + @utils.deprecated("Interaction.original_response", "2.2") async def original_message(self): """An alias for :meth:`original_response`. @@ -341,7 +341,7 @@ async def original_message(self): ClientException The channel for the message could not be resolved. """ - return self.original_response() + return await self.original_response() async def edit_original_response( self, @@ -447,7 +447,7 @@ async def edit_original_response( return message - @utils.deprecated("Interaction.edit_original_response", "2.1") + @utils.deprecated("Interaction.edit_original_response", "2.2") async def edit_original_message(self, **kwargs): """An alias for :meth:`edit_original_response`. @@ -467,7 +467,7 @@ async def edit_original_message(self, **kwargs): ValueError The length of ``embeds`` was invalid. """ - return self.edit_original_response(**kwargs) + return await self.edit_original_response(**kwargs) async def delete_original_response(self, *, delay: float | None = None) -> None: """|coro| @@ -505,7 +505,7 @@ async def delete_original_response(self, *, delay: float | None = None) -> None: else: await func - @utils.deprecated("Interaction.delete_original_response", "2.1") + @utils.deprecated("Interaction.delete_original_response", "2.2") async def delete_original_message(self, **kwargs): """An alias for :meth:`delete_original_response`. @@ -516,7 +516,7 @@ async def delete_original_message(self, **kwargs): Forbidden Deleted a message that is not yours. """ - return self.delete_original_response(**kwargs) + return await self.delete_original_response(**kwargs) def to_dict(self) -> dict[str, Any]: """ @@ -586,7 +586,7 @@ def __init__(self, parent: Interaction): self._response_lock = asyncio.Lock() def is_done(self) -> bool: - """:class:`bool`: Indicates whether an interaction response has been done before. + """Indicates whether an interaction response has been done before. An interaction can only be responded to once. """ diff --git a/discord/invite.py b/discord/invite.py index 713a2d63..2560bf99 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -109,12 +109,12 @@ def __repr__(self) -> str: @property def mention(self) -> str: - """:class:`str`: The string that allows you to mention the channel.""" + """The string that allows you to mention the channel.""" return f"<#{self.id}>" @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the channel's creation time in UTC.""" + """Returns the channel's creation time in UTC.""" return snowflake_time(self.id) @@ -192,19 +192,19 @@ def __repr__(self) -> str: @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the guild's creation time in UTC.""" + """Returns the guild's creation time in UTC.""" return snowflake_time(self.id) @property def icon(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the guild's icon asset, if available.""" + """Returns the guild's icon asset, if available.""" if self._icon is None: return None return Asset._from_guild_icon(self._state, self.id, self._icon) @property def banner(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the guild's banner asset, if available.""" + """Returns the guild's banner asset, if available.""" if self._banner is None: return None return Asset._from_guild_image( @@ -213,7 +213,7 @@ def banner(self) -> Asset | None: @property def splash(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available.""" + """Returns the guild's invite splash asset, if available.""" if self._splash is None: return None return Asset._from_guild_image( @@ -498,12 +498,12 @@ def __hash__(self) -> int: @property def id(self) -> str: - """:class:`str`: Returns the proper code portion of the invite.""" + """Returns the proper code portion of the invite.""" return self.code @property def url(self) -> str: - """:class:`str`: A property that retrieves the invite URL.""" + """A property that retrieves the invite URL.""" return f"{self.BASE}/{self.code}{f'?event={self.scheduled_event.id}' if self.scheduled_event else ''}" async def delete(self, *, reason: str | None = None): diff --git a/discord/iterators.py b/discord/iterators.py index 181527f7..f4ec6cda 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -635,7 +635,6 @@ async def _retrieve_guilds_after_strategy(self, retrieve): class MemberIterator(_AsyncIterator["Member"]): def __init__(self, guild, limit=1000, after=None): - if isinstance(after, datetime.datetime): after = Object(id=time_snowflake(after, high=True)) diff --git a/discord/member.py b/discord/member.py index 628002a7..0c798a72 100644 --- a/discord/member.py +++ b/discord/member.py @@ -310,7 +310,7 @@ def __init__( ) self._roles: utils.SnowflakeList = utils.SnowflakeList(map(int, data["roles"])) self._client_status: dict[str | None, str] = {None: "offline"} - self.activities: tuple[ActivityTypes, ...] = tuple() + self.activities: tuple[ActivityTypes, ...] = () self.nick: str | None = data.get("nick", None) self.pending: bool = data.get("pending", False) self._avatar: str | None = data.get("avatar") @@ -323,7 +323,8 @@ def __str__(self) -> str: def __repr__(self) -> str: return ( - f"" ) @@ -442,14 +443,14 @@ def _update_inner_user(self, user: UserPayload) -> tuple[User, User] | None: @property def status(self) -> Status: - """:class:`Status`: The member's overall status. + """The member's overall status. If the value is unknown, then it will be a :class:`str` instead. """ return try_enum(Status, self._client_status[None]) @property def raw_status(self) -> str: - """:class:`str`: The member's overall status as a string value. + """The member's overall status as a string value. .. versionadded:: 1.5 """ @@ -462,26 +463,26 @@ def status(self, value: Status) -> None: @property def mobile_status(self) -> Status: - """:class:`Status`: The member's status on a mobile device, if applicable.""" + """The member's status on a mobile device, if applicable.""" return try_enum(Status, self._client_status.get("mobile", "offline")) @property def desktop_status(self) -> Status: - """:class:`Status`: The member's status on the desktop client, if applicable.""" + """The member's status on the desktop client, if applicable.""" return try_enum(Status, self._client_status.get("desktop", "offline")) @property def web_status(self) -> Status: - """:class:`Status`: The member's status on the web client, if applicable.""" + """The member's status on the web client, if applicable.""" return try_enum(Status, self._client_status.get("web", "offline")) def is_on_mobile(self) -> bool: - """:class:`bool`: A helper function that determines if a member is active on a mobile device.""" + """A helper function that determines if a member is active on a mobile device.""" return "mobile" in self._client_status @property def colour(self) -> Colour: - """:class:`Colour`: A property that returns a colour denoting the rendered colour + """A property that returns a colour denoting the rendered colour for the member. If the default colour is the one rendered then an instance of :meth:`Colour.default` is returned. @@ -500,7 +501,7 @@ def colour(self) -> Colour: @property def color(self) -> Colour: - """:class:`Colour`: A property that returns a color denoting the rendered color for + """A property that returns a color denoting the rendered color for the member. If the default color is the one rendered then an instance of :meth:`Colour.default` is returned. @@ -510,7 +511,7 @@ def color(self) -> Colour: @property def roles(self) -> list[Role]: - """List[:class:`Role`]: A :class:`list` of :class:`Role` that the member belongs to. Note + """A :class:`list` of :class:`Role` that the member belongs to. Note that the first element of this list is always the default '@everyone' role. @@ -528,12 +529,12 @@ def roles(self) -> list[Role]: @property def mention(self) -> str: - """:class:`str`: Returns a string that allows you to mention the member.""" + """Returns a string that allows you to mention the member.""" return f"<@{self._user.id}>" @property def display_name(self) -> str: - """:class:`str`: Returns the user's display name. + """Returns the user's display name. For regular users this is just their username, but if they have a guild specific nickname then that @@ -543,7 +544,7 @@ def display_name(self) -> str: @property def display_avatar(self) -> Asset: - """:class:`Asset`: Returns the member's display avatar. + """Returns the member's display avatar. For regular members this is just their avatar, but if they have a guild specific avatar then that @@ -555,7 +556,7 @@ def display_avatar(self) -> Asset: @property def guild_avatar(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild avatar + """Returns an :class:`Asset` for the guild avatar the member has. If unavailable, ``None`` is returned. .. versionadded:: 2.0 @@ -568,7 +569,7 @@ def guild_avatar(self) -> Asset | None: @property def activity(self) -> ActivityTypes | None: - """Optional[Union[:class:`BaseActivity`, :class:`Spotify`]]: Returns the primary + """Returns the primary activity the user is currently doing. Could be ``None`` if no activity is being done. .. note:: @@ -607,7 +608,7 @@ def mentioned_in(self, message: Message) -> bool: @property def top_role(self) -> Role: - """:class:`Role`: Returns the member's highest role. + """Returns the member's highest role. This is useful for figuring where a member stands in the role hierarchy chain. @@ -620,7 +621,7 @@ def top_role(self) -> Role: @property def guild_permissions(self) -> Permissions: - """:class:`Permissions`: Returns the member's guild permissions. + """Returns the member's guild permissions. This only takes into consideration the guild permissions and not most of the implied permissions or any of the @@ -645,12 +646,12 @@ def guild_permissions(self) -> Permissions: @property def voice(self) -> VoiceState | None: - """Optional[:class:`VoiceState`]: Returns the member's current voice state.""" + """Returns the member's current voice state.""" return self.guild._voice_state_for(self._user.id) @property def timed_out(self) -> bool: - """bool: Returns whether the member is timed out. + """Returns whether the member is timed out. .. versionadded:: 2.0 """ diff --git a/discord/message.py b/discord/message.py index e98378a2..e2991133 100644 --- a/discord/message.py +++ b/discord/message.py @@ -110,7 +110,8 @@ def convert_emoji_reaction(emoji): return emoji.strip("<>") raise InvalidArgument( - f"emoji argument must be str, Emoji, or Reaction not {emoji.__class__.__name__}." + "emoji argument must be str, Emoji, or Reaction not" + f" {emoji.__class__.__name__}." ) @@ -198,7 +199,7 @@ def __init__(self, *, data: AttachmentPayload, state: ConnectionState): self.description: str | None = data.get("description") def is_spoiler(self) -> bool: - """:class:`bool`: Whether this attachment contains a spoiler.""" + """Whether this attachment contains a spoiler.""" return self.filename.startswith("SPOILER_") def __repr__(self) -> str: @@ -375,22 +376,25 @@ def __init__(self, parent: MessageReference): self._parent: MessageReference = parent def __repr__(self) -> str: - return f"" + return ( + "" + ) @property def id(self) -> int: - """:class:`int`: The message ID of the deleted referenced message.""" + """The message ID of the deleted referenced message.""" # the parent's message id won't be None here return self._parent.message_id # type: ignore @property def channel_id(self) -> int: - """:class:`int`: The channel ID of the deleted referenced message.""" + """The channel ID of the deleted referenced message.""" return self._parent.channel_id @property def guild_id(self) -> int | None: - """Optional[:class:`int`]: The guild ID of the deleted referenced message.""" + """The guild ID of the deleted referenced message.""" return self._parent.guild_id @@ -499,12 +503,12 @@ def from_message( @property def cached_message(self) -> Message | None: - """Optional[:class:`~discord.Message`]: The cached message, if found in the internal message cache.""" + """The cached message, if found in the internal message cache.""" return self._state and self._state._get_message(self.message_id) @property def jump_url(self) -> str: - """:class:`str`: Returns a URL that allows the client to jump to the referenced message. + """Returns a URL that allows the client to jump to the referenced message. .. versionadded:: 1.7 """ @@ -982,7 +986,7 @@ def _rebind_cached_references( @utils.cached_slot_property("_cs_raw_mentions") def raw_mentions(self) -> list[int]: - """List[:class:`int`]: A property that returns an array of user IDs matched with + """A property that returns an array of user IDs matched with the syntax of ``<@user_id>`` in the message content. This allows you to receive the user IDs of mentioned users @@ -992,14 +996,14 @@ def raw_mentions(self) -> list[int]: @utils.cached_slot_property("_cs_raw_channel_mentions") def raw_channel_mentions(self) -> list[int]: - """List[:class:`int`]: A property that returns an array of channel IDs matched with + """A property that returns an array of channel IDs matched with the syntax of ``<#channel_id>`` in the message content. """ return [int(x) for x in re.findall(r"<#([0-9]{15,20})>", self.content)] @utils.cached_slot_property("_cs_raw_role_mentions") def raw_role_mentions(self) -> list[int]: - """List[:class:`int`]: A property that returns an array of role IDs matched with + """A property that returns an array of role IDs matched with the syntax of ``<@&role_id>`` in the message content. """ return [int(x) for x in re.findall(r"<@&([0-9]{15,20})>", self.content)] @@ -1013,7 +1017,7 @@ def channel_mentions(self) -> list[GuildChannel]: @utils.cached_slot_property("_cs_clean_content") def clean_content(self) -> str: - """:class:`str`: A property that returns the content in a "cleaned up" + """A property that returns the content in a "cleaned up" manner. This basically means that mentions are transformed into the way the client shows it. e.g. ``<#id>`` will transform into ``#name``. @@ -1063,24 +1067,24 @@ def repl(obj): @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: The message's creation time in UTC.""" + """The message's creation time in UTC.""" return utils.snowflake_time(self.id) @property def edited_at(self) -> datetime.datetime | None: - """Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the + """An aware UTC datetime object containing the edited time of the message. """ return self._edited_timestamp @property def jump_url(self) -> str: - """:class:`str`: Returns a URL that allows the client to jump to this message.""" + """Returns a URL that allows the client to jump to this message.""" guild_id = getattr(self.guild, "id", "@me") return f"https://discord.com/channels/{guild_id}/{self.channel.id}/{self.id}" def is_system(self) -> bool: - """:class:`bool`: Whether the message is a system message. + """Whether the message is a system message. A system message is a message that is constructed entirely by the Discord API in response to something. @@ -1095,8 +1099,8 @@ def is_system(self) -> bool: ) @utils.cached_slot_property("_cs_system_content") - def system_content(self): - r""":class:`str`: A property that returns the content that is rendered + def system_content(self) -> str: + r"""A property that returns the content that is rendered regardless of the :attr:`Message.type`. In the case of :attr:`MessageType.default` and :attr:`MessageType.reply`\, @@ -1117,9 +1121,15 @@ def system_content(self): if self.type is MessageType.recipient_remove: if self.channel.type is ChannelType.group: - return f"{self.author.name} removed {self.mentions[0].name} from the group." + return ( + f"{self.author.name} removed {self.mentions[0].name} from the" + " group." + ) else: - return f"{self.author.name} removed {self.mentions[0].name} from the thread." + return ( + f"{self.author.name} removed {self.mentions[0].name} from the" + " thread." + ) if self.type is MessageType.channel_name_change: return f"{self.author.name} changed the channel name: **{self.content}**" @@ -1154,33 +1164,45 @@ def system_content(self): if not self.content: return f"{self.author.name} just boosted the server!" else: - return f"{self.author.name} just boosted the server **{self.content}** times!" + return ( + f"{self.author.name} just boosted the server **{self.content}**" + " times!" + ) if self.type is MessageType.premium_guild_tier_1: if not self.content: - return f"{self.author.name} just boosted the server! {self.guild} has achieved **Level 1!**" + return ( + f"{self.author.name} just boosted the server! {self.guild} has" + " achieved **Level 1!**" + ) else: return ( - f"{self.author.name} just boosted the server **{self.content}** times!" - f" {self.guild} has achieved **Level 1!**" + f"{self.author.name} just boosted the server **{self.content}**" + f" times! {self.guild} has achieved **Level 1!**" ) if self.type is MessageType.premium_guild_tier_2: if not self.content: - return f"{self.author.name} just boosted the server! {self.guild} has achieved **Level 2!**" + return ( + f"{self.author.name} just boosted the server! {self.guild} has" + " achieved **Level 2!**" + ) else: return ( - f"{self.author.name} just boosted the server **{self.content}** times!" - f" {self.guild} has achieved **Level 2!**" + f"{self.author.name} just boosted the server **{self.content}**" + f" times! {self.guild} has achieved **Level 2!**" ) if self.type is MessageType.premium_guild_tier_3: if not self.content: - return f"{self.author.name} just boosted the server! {self.guild} has achieved **Level 3!**" + return ( + f"{self.author.name} just boosted the server! {self.guild} has" + " achieved **Level 3!**" + ) else: return ( - f"{self.author.name} just boosted the server **{self.content}** times!" - f" {self.guild} has achieved **Level 3!**" + f"{self.author.name} just boosted the server **{self.content}**" + f" times! {self.guild} has achieved **Level 3!**" ) if self.type is MessageType.channel_follow_add: @@ -1192,27 +1214,36 @@ def system_content(self): if self.type is MessageType.guild_discovery_disqualified: return ( - "This server has been removed from Server Discovery because it no longer passes all the" - " requirements. Check Server Settings for more details." + "This server has been removed from Server Discovery because it no" + " longer passes all the requirements. Check Server Settings for more" + " details." ) if self.type is MessageType.guild_discovery_requalified: - return "This server is eligible for Server Discovery again and has been automatically relisted!" + return ( + "This server is eligible for Server Discovery again and has been" + " automatically relisted!" + ) if self.type is MessageType.guild_discovery_grace_period_initial_warning: return ( - "This server has failed Discovery activity requirements for 1 week. If this server fails for" - " 4 weeks in a row, it will be automatically removed from Discovery." + "This server has failed Discovery activity requirements for 1 week. If" + " this server fails for 4 weeks in a row, it will be automatically" + " removed from Discovery." ) if self.type is MessageType.guild_discovery_grace_period_final_warning: return ( - "This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails" - " for 1 more week, it will be removed from Discovery." + "This server has failed Discovery activity requirements for 3 weeks in" + " a row. If this server fails for 1 more week, it will be removed from" + " Discovery." ) if self.type is MessageType.thread_created: - return f"{self.author.name} started a thread: **{self.content}**. See all **threads**." + return ( + f"{self.author.name} started a thread: **{self.content}**. See all" + " **threads**." + ) if self.type is MessageType.reply: return self.content @@ -1225,7 +1256,10 @@ def system_content(self): return self.reference.resolved.content # type: ignore if self.type is MessageType.guild_invite_reminder: - return "Wondering who to invite?\nStart by inviting anyone who can help you build the server!" + return ( + "Wondering who to invite?\nStart by inviting anyone who can help you" + " build the server!" + ) async def delete( self, *, delay: float | None = None, reason: str | None = None @@ -1817,7 +1851,8 @@ def __init__(self, *, channel: PartialMessageableChannel, id: int): ChannelType.private_thread, ): raise TypeError( - f"Expected TextChannel, VoiceChannel, DMChannel or Thread not {type(channel)!r}" + "Expected TextChannel, VoiceChannel, DMChannel or Thread not" + f" {type(channel)!r}" ) self.channel: PartialMessageableChannel = channel @@ -1838,12 +1873,12 @@ def __repr__(self) -> str: @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: The partial message's creation time in UTC.""" + """The partial message's creation time in UTC.""" return utils.snowflake_time(self.id) @utils.cached_slot_property("_cs_guild") def guild(self) -> Guild | None: - """Optional[:class:`Guild`]: The guild that the partial message belongs to, if applicable.""" + """The guild that the partial message belongs to, if applicable.""" return getattr(self.channel, "guild", None) async def fetch(self) -> Message: diff --git a/discord/object.py b/discord/object.py index a189b44d..d8bd7226 100644 --- a/discord/object.py +++ b/discord/object.py @@ -48,7 +48,7 @@ class Object(Hashable): objects (if any) actually inherit from this class. There are also some cases where some WebSocket events are received - in :issue:`strange order <21>` and when such events happened you would + in :dpy-issue:`strange order <21>` and when such events happened you would receive this class rather than the actual data class. These cases are extremely rare. @@ -87,20 +87,20 @@ def __repr__(self) -> str: @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the snowflake's creation time in UTC.""" + """Returns the snowflake's creation time in UTC.""" return utils.snowflake_time(self.id) @property def worker_id(self) -> int: - """:class:`int`: Returns the worker id that made the snowflake.""" + """Returns the worker id that made the snowflake.""" return (self.id & 0x3E0000) >> 17 @property def process_id(self) -> int: - """:class:`int`: Returns the process id that made the snowflake.""" + """Returns the process id that made the snowflake.""" return (self.id & 0x1F000) >> 12 @property def increment_id(self) -> int: - """:class:`int`: Returns the increment id that made the snowflake.""" + """Returns the increment id that made the snowflake.""" return self.id & 0xFFF diff --git a/discord/opus.py b/discord/opus.py index 42867e50..63362b92 100644 --- a/discord/opus.py +++ b/discord/opus.py @@ -147,7 +147,7 @@ def _err_ne(result: T, func: Callable, args: list) -> T: # The fourth is the error handler. exported_functions: list[tuple[Any, ...]] = [ # Generic - ("opus_get_version_string", None, ctypes.c_char_p, None), + ("opus_get_version_string", [], ctypes.c_char_p, None), ("opus_strerror", [ctypes.c_int], ctypes.c_char_p, None), # Encoder functions ("opus_encoder_get_size", [ctypes.c_int], ctypes.c_int, None), @@ -169,7 +169,7 @@ def _err_ne(result: T, func: Callable, args: list) -> T: ctypes.c_int32, _err_lt, ), - ("opus_encoder_ctl", None, ctypes.c_int32, _err_lt), + ("opus_encoder_ctl", [EncoderStructPtr, ctypes.c_int], ctypes.c_int32, _err_lt), ("opus_encoder_destroy", [EncoderStructPtr], None, None), # Decoder functions ("opus_decoder_get_size", [ctypes.c_int], ctypes.c_int, None), @@ -205,7 +205,7 @@ def _err_ne(result: T, func: Callable, args: list) -> T: ctypes.c_int, _err_lt, ), - ("opus_decoder_ctl", None, ctypes.c_int32, _err_lt), + ("opus_decoder_ctl", [DecoderStructPtr, ctypes.c_int], ctypes.c_int32, _err_lt), ("opus_decoder_destroy", [DecoderStructPtr], None, None), ( "opus_decoder_get_nb_samples", @@ -397,7 +397,8 @@ def set_bitrate(self, kbps: int) -> int: def set_bandwidth(self, req: BAND_CTL) -> None: if req not in band_ctl: raise KeyError( - f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(band_ctl)}' + f"{req!r} is not a valid bandwidth setting. Try one of:" + f" {','.join(band_ctl)}" ) k = band_ctl[req] @@ -406,7 +407,8 @@ def set_bandwidth(self, req: BAND_CTL) -> None: def set_signal_type(self, req: SIGNAL_CTL) -> None: if req not in signal_ctl: raise KeyError( - f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(signal_ctl)}' + f"{req!r} is not a valid bandwidth setting. Try one of:" + f" {','.join(signal_ctl)}" ) k = signal_ctl[req] diff --git a/discord/partial_emoji.py b/discord/partial_emoji.py index 32fd013e..dd2d6fa0 100644 --- a/discord/partial_emoji.py +++ b/discord/partial_emoji.py @@ -26,7 +26,7 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, TypedDict, TypeVar from . import utils from .asset import Asset, AssetMixin @@ -160,6 +160,16 @@ def to_dict(self) -> dict[str, Any]: def _to_partial(self) -> PartialEmoji: return self + def _to_forum_tag_payload( + self, + ) -> TypedDict("TagPayload", {"emoji_id": int, "emoji_name": None}) | TypedDict( + "TagPayload", {"emoji_id": None, "emoji_name": str} + ): + if self.id is None: + return {"emoji_id": None, "emoji_name": self.name} + else: + return {"emoji_id": self.id, "emoji_name": None} + @classmethod def with_state( cls: type[PE], @@ -174,11 +184,12 @@ def with_state( return self def __str__(self) -> str: + # Emoji won't render if the name is empty + name = self.name or "_" if self.id is None: - return self.name - if self.animated: - return f"" - return f"<:{self.name}:{self.id}>" + return name + animated_tag = "a" if self.animated else "" + return f"<{animated_tag}:{name}:{self.id}>" def __repr__(self): return f"<{self.__class__.__name__} animated={self.animated} name={self.name!r} id={self.id}>" @@ -198,11 +209,11 @@ def __hash__(self) -> int: return hash((self.id, self.name)) def is_custom_emoji(self) -> bool: - """:class:`bool`: Checks if this is a custom non-Unicode emoji.""" + """Checks if this is a custom non-Unicode emoji.""" return self.id is not None def is_unicode_emoji(self) -> bool: - """:class:`bool`: Checks if this is a Unicode emoji.""" + """Checks if this is a Unicode emoji.""" return self.id is None def _as_reaction(self) -> str: @@ -212,7 +223,7 @@ def _as_reaction(self) -> str: @property def created_at(self) -> datetime | None: - """Optional[:class:`datetime.datetime`]: Returns the emoji's creation time in UTC, or None if Unicode emoji. + """Returns the emoji's creation time in UTC, or None if Unicode emoji. .. versionadded:: 1.6 """ @@ -223,7 +234,7 @@ def created_at(self) -> datetime | None: @property def url(self) -> str: - """:class:`str`: Returns the URL of the emoji, if it is custom. + """Returns the URL of the emoji, if it is custom. If this isn't a custom emoji then an empty string is returned """ diff --git a/discord/permissions.py b/discord/permissions.py index 1753235a..43dc6f2f 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -125,7 +125,8 @@ class Permissions(BaseFlags): def __init__(self, permissions: int = 0, **kwargs: bool): if not isinstance(permissions, int): raise TypeError( - f"Expected int parameter, received {permissions.__class__.__name__} instead." + "Expected int parameter, received" + f" {permissions.__class__.__name__} instead." ) self.value = permissions @@ -140,7 +141,8 @@ def is_subset(self, other: Permissions) -> bool: return (self.value & other.value) == self.value else: raise TypeError( - f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}" + f"cannot compare {self.__class__.__name__} with" + f" {other.__class__.__name__}" ) def is_superset(self, other: Permissions) -> bool: @@ -149,7 +151,8 @@ def is_superset(self, other: Permissions) -> bool: return (self.value | other.value) == self.value else: raise TypeError( - f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}" + f"cannot compare {self.__class__.__name__} with" + f" {other.__class__.__name__}" ) def is_strict_subset(self, other: Permissions) -> bool: @@ -749,7 +752,7 @@ def _set(self, key: str, value: bool | None) -> None: self._values[key] = value def pair(self) -> tuple[Permissions, Permissions]: - """Tuple[:class:`Permissions`, :class:`Permissions`]: Returns the (allow, deny) pair from this overwrite.""" + """Returns the (allow, deny) pair from this overwrite.""" allow = Permissions.none() deny = Permissions.none() diff --git a/discord/player.py b/discord/player.py index 9ba37661..87b0f571 100644 --- a/discord/player.py +++ b/discord/player.py @@ -101,7 +101,7 @@ def read(self) -> bytes: raise NotImplementedError def is_opus(self) -> bool: - """:class:`bool`: Checks if the audio source is already encoded in Opus.""" + """Checks if the audio source is already encoded in Opus.""" return False def cleanup(self) -> None: @@ -154,7 +154,8 @@ def __init__( piping = subprocess_kwargs.get("stdin") == subprocess.PIPE if piping and isinstance(source, str): raise TypeError( - "parameter conflict: 'source' parameter cannot be a string when piping to stdin" + "parameter conflict: 'source' parameter cannot be a string when piping" + " to stdin" ) args = [executable, *args] @@ -393,7 +394,6 @@ def __init__( before_options=None, options=None, ) -> None: - args = [] subprocess_kwargs = { "stdin": subprocess.PIPE if pipe else subprocess.DEVNULL, @@ -680,7 +680,7 @@ def __init__(self, original: AT, volume: float = 1.0): @property def volume(self) -> float: - """:class:`float`: Retrieves or sets the volume as a floating point percentage (e.g. ``1.0`` for 100%).""" + """Retrieves or sets the volume as a floating point percentage (e.g. ``1.0`` for 100%).""" return self._volume @volume.setter diff --git a/discord/raw_models.py b/discord/raw_models.py index 65f9d8f1..22b1341a 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -28,8 +28,9 @@ import datetime from typing import TYPE_CHECKING -from .automod import AutoModAction +from .automod import AutoModAction, AutoModTriggerType from .enums import ChannelType, try_enum +from .types.user import User if TYPE_CHECKING: from .abc import MessageableChannel @@ -43,6 +44,7 @@ from .types.raw_models import ( BulkMessageDeleteEvent, IntegrationDeleteEvent, + MemberRemoveEvent, MessageDeleteEvent, MessageUpdateEvent, ReactionActionEvent, @@ -50,6 +52,8 @@ ReactionClearEvent, ScheduledEventSubscription, ThreadDeleteEvent, + ThreadMembersUpdateEvent, + ThreadUpdateEvent, TypingEvent, ) @@ -62,10 +66,13 @@ "RawReactionClearEvent", "RawReactionClearEmojiEvent", "RawIntegrationDeleteEvent", + "RawThreadUpdateEvent", "RawThreadDeleteEvent", "RawTypingEvent", + "RawMemberRemoveEvent", "RawScheduledEventSubscription", "AutoModActionExecutionEvent", + "RawThreadMembersUpdateEvent", ) @@ -305,6 +312,38 @@ def __init__(self, data: IntegrationDeleteEvent) -> None: self.application_id: int | None = None +class RawThreadUpdateEvent(_RawReprMixin): + """Represents the payload for an :func:`on_raw_thread_update` event. + + .. versionadded:: 2.4 + + Attributes + ---------- + thread_id: :class:`int` + The ID of the updated thread. + thread_type: :class:`discord.ChannelType` + The channel type of the updated thread. + guild_id: :class:`int` + The ID of the guild the thread belongs to. + parent_id: :class:`int` + The ID of the channel the thread belongs to. + data: :class:`dict` + The raw data given by the `gateway `_. + thread: :class:`discord.Thread` | None + The thread, if it could be found in the internal cache. + """ + + __slots__ = ("thread_id", "thread_type", "parent_id", "guild_id", "data", "thread") + + def __init__(self, data: ThreadUpdateEvent) -> None: + self.thread_id: int = int(data["id"]) + self.thread_type: ChannelType = try_enum(ChannelType, data["type"]) + self.guild_id: int = int(data["guild_id"]) + self.parent_id: int = int(data["parent_id"]) + self.data: ThreadUpdateEvent = data + self.thread: Thread | None = None + + class RawThreadDeleteEvent(_RawReprMixin): """Represents the payload for :func:`on_raw_thread_delete` event. @@ -370,6 +409,26 @@ def __init__(self, data: TypingEvent) -> None: self.guild_id: int | None = None +class RawMemberRemoveEvent(_RawReprMixin): + """Represents the payload for an :func:`on_raw_member_remove` event. + + .. versionadded:: 2.4 + + Attributes + ---------- + user: :class:`discord.User` + The user that left the guild. + guild_id: :class:`int` + The ID of the guild the user left. + """ + + __slots__ = ("user", "guild_id") + + def __init__(self, data: MemberRemoveEvent, user: User): + self.user: User = user + self.guild_id: int = int(data["guild_id"]) + + class RawScheduledEventSubscription(_RawReprMixin): """Represents the payload for a :func:`raw_scheduled_event_user_add` or :func:`raw_scheduled_event_user_remove` event. @@ -409,6 +468,10 @@ class AutoModActionExecutionEvent: The action that was executed. rule_id: :class:`int` The ID of the rule that the action belongs to. + rule_trigger_type: :class:`AutoModTriggerType` + The category of trigger the rule belongs to. + + .. versionadded:: 2.4 guild_id: :class:`int` The ID of the guild that the action was executed in. guild: Optional[:class:`Guild`] @@ -443,6 +506,7 @@ class AutoModActionExecutionEvent: __slots__ = ( "action", "rule_id", + "rule_trigger_type", "guild_id", "guild", "user_id", @@ -461,6 +525,9 @@ class AutoModActionExecutionEvent: def __init__(self, state: ConnectionState, data: AutoModActionExecution) -> None: self.action: AutoModAction = AutoModAction.from_dict(data["action"]) self.rule_id: int = int(data["rule_id"]) + self.rule_trigger_type: AutoModTriggerType = try_enum( + AutoModTriggerType, int(data["rule_trigger_type"]) + ) self.guild_id: int = int(data["guild_id"]) self.guild: Guild | None = state._get_guild(self.guild_id) self.user_id: int = int(data["user_id"]) @@ -508,3 +575,29 @@ def __repr__(self) -> str: f"rule_id={self.rule_id!r} guild_id={self.guild_id!r} " f"user_id={self.user_id!r}>" ) + + +class RawThreadMembersUpdateEvent(_RawReprMixin): + """Represents the payload for an :func:`on_raw_thread_member_remove` event. + + .. versionadded:: 2.4 + + Attributes + ---------- + thread_id: :class:`int` + The ID of the thread that was updated. + guild_id: :class:`int` + The ID of the guild the thread is in. + member_count: :class:`int` + The approximate number of members in the thread. Maximum of 50. + data: :class:`dict` + The raw data given by the `gateway `_. + """ + + __slots__ = ("thread_id", "guild_id", "member_count", "data") + + def __init__(self, data: ThreadMembersUpdateEvent) -> None: + self.thread_id = int(data["id"]) + self.guild_id = int(data["guild_id"]) + self.member_count = int(data["member_count"]) + self.data = data diff --git a/discord/reaction.py b/discord/reaction.py index 2a9e1aa4..ea1bcbde 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -95,7 +95,7 @@ def __init__( # TODO: typeguard def is_custom_emoji(self) -> bool: - """:class:`bool`: If this is a custom emoji.""" + """If this is a custom emoji.""" return not isinstance(self.emoji, str) def __eq__(self, other: Any) -> bool: diff --git a/discord/role.py b/discord/role.py index 6a9b3038..4012d29c 100644 --- a/discord/role.py +++ b/discord/role.py @@ -85,15 +85,15 @@ def __init__(self, data: RoleTagPayload): self._premium_subscriber: Any | None = data.get("premium_subscriber", MISSING) def is_bot_managed(self) -> bool: - """:class:`bool`: Whether the role is associated with a bot.""" + """Whether the role is associated with a bot.""" return self.bot_id is not None def is_premium_subscriber(self) -> bool: - """:class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild.""" + """Whether the role is the premium subscriber, AKA "boost", role for the guild.""" return self._premium_subscriber is None def is_integration(self) -> bool: - """:class:`bool`: Whether the role is managed by an integration.""" + """Whether the role is managed by an integration.""" return self.integration_id is not None def __repr__(self) -> str: @@ -261,32 +261,32 @@ def _update(self, data: RolePayload): self.tags = None def is_default(self) -> bool: - """:class:`bool`: Checks if the role is the default role.""" + """Checks if the role is the default role.""" return self.guild.id == self.id def is_bot_managed(self) -> bool: - """:class:`bool`: Whether the role is associated with a bot. + """Whether the role is associated with a bot. .. versionadded:: 1.6 """ return self.tags is not None and self.tags.is_bot_managed() def is_premium_subscriber(self) -> bool: - """:class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild. + """Whether the role is the premium subscriber, AKA "boost", role for the guild. .. versionadded:: 1.6 """ return self.tags is not None and self.tags.is_premium_subscriber() def is_integration(self) -> bool: - """:class:`bool`: Whether the role is managed by an integration. + """Whether the role is managed by an integration. .. versionadded:: 1.6 """ return self.tags is not None and self.tags.is_integration() def is_assignable(self) -> bool: - """:class:`bool`: Whether the role is able to be assigned or removed by the bot. + """Whether the role is able to be assigned or removed by the bot. .. versionadded:: 2.0 """ @@ -299,32 +299,32 @@ def is_assignable(self) -> bool: @property def permissions(self) -> Permissions: - """:class:`Permissions`: Returns the role's permissions.""" + """Returns the role's permissions.""" return Permissions(self._permissions) @property def colour(self) -> Colour: - """:class:`Colour`: Returns the role colour. An alias exists under ``color``.""" + """Returns the role colour. An alias exists under ``color``.""" return Colour(self._colour) @property def color(self) -> Colour: - """:class:`Colour`: Returns the role color. An alias exists under ``colour``.""" + """Returns the role color. An alias exists under ``colour``.""" return self.colour @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the role's creation time in UTC.""" + """Returns the role's creation time in UTC.""" return snowflake_time(self.id) @property def mention(self) -> str: - """:class:`str`: Returns a string that allows you to mention a role.""" + """Returns a string that allows you to mention a role.""" return f"<@&{self.id}>" @property def members(self) -> list[Member]: - """List[:class:`Member`]: Returns all the members with this role.""" + """Returns all the members with this role.""" all_members = self.guild.members if self.is_default(): return all_members @@ -334,7 +334,7 @@ def members(self) -> list[Member]: @property def icon(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the role's icon asset, if available. + """Returns the role's icon asset, if available. .. versionadded:: 2.0 """ diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index e4c9e218..8fefdc2c 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -239,7 +239,7 @@ def __repr__(self) -> str: @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the scheduled event's creation time in UTC.""" + """Returns the scheduled event's creation time in UTC.""" return utils.snowflake_time(self.id) @property @@ -249,12 +249,12 @@ def interested(self) -> int | None: @property def url(self) -> str: - """:class:`str`: The url to reference the scheduled event.""" + """The url to reference the scheduled event.""" return f"https://discord.com/events/{self.guild.id}/{self.id}" @property def cover(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the scheduled event cover image asset, if available.""" + """Returns the scheduled event cover image asset, if available.""" if self._cover is None: return None return Asset._from_scheduled_event_cover( diff --git a/discord/shard.py b/discord/shard.py index 476e0b85..8e84f5b8 100644 --- a/discord/shard.py +++ b/discord/shard.py @@ -257,7 +257,7 @@ def __init__(self, parent: Shard, shard_count: int | None) -> None: self.shard_count: int | None = shard_count def is_closed(self) -> bool: - """:class:`bool`: Whether the shard connection is currently closed.""" + """Whether the shard connection is currently closed.""" return not self._parent.ws.open async def disconnect(self) -> None: @@ -294,11 +294,11 @@ async def connect(self) -> None: @property def latency(self) -> float: - """:class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds for this shard.""" + """Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds for this shard.""" return self._parent.ws.latency def is_ws_ratelimited(self) -> bool: - """:class:`bool`: Whether the websocket is currently rate limited. + """Whether the websocket is currently rate limited. This can be useful to know when deciding whether you should query members using HTTP or via the gateway. @@ -383,7 +383,7 @@ def _get_state(self, **options: Any) -> AutoShardedConnectionState: @property def latency(self) -> float: - """:class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. + """Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. This operates similarly to :meth:`Client.latency` except it uses the average latency of every shard's latency. To get a list of shard latency, check the @@ -395,7 +395,7 @@ def latency(self) -> float: @property def latencies(self) -> list[tuple[int, float]]: - """List[Tuple[:class:`int`, :class:`float`]]: A list of latencies between a + """A list of latencies between a HEARTBEAT and a HEARTBEAT_ACK in seconds. This returns a list of tuples with elements ``(shard_id, latency)``. @@ -405,7 +405,7 @@ def latencies(self) -> list[tuple[int, float]]: ] def get_shard(self, shard_id: int) -> ShardInfo | None: - """Optional[:class:`ShardInfo`]: Gets the shard information at a given shard ID or ``None`` if not found.""" + """Gets the shard information at a given shard ID or ``None`` if not found.""" try: parent = self.__shards[shard_id] except KeyError: @@ -415,7 +415,7 @@ def get_shard(self, shard_id: int) -> ShardInfo | None: @property def shards(self) -> dict[int, ShardInfo]: - """Mapping[:class:`int`, :class:`ShardInfo`]: Returns a mapping of shard IDs to their respective info object.""" + """Returns a mapping of shard IDs to their respective info object.""" return { shard_id: ShardInfo(parent, self.shard_count) for shard_id, parent in self.__shards.items() @@ -571,7 +571,7 @@ async def change_presence( me.status = status_enum def is_ws_ratelimited(self) -> bool: - """:class:`bool`: Whether the websocket is currently rate limited. + """Whether the websocket is currently rate limited. This can be useful to know when deciding whether you should query members using HTTP or via the gateway. diff --git a/discord/stage_instance.py b/discord/stage_instance.py index fa30bd72..8864a2f5 100644 --- a/discord/stage_instance.py +++ b/discord/stage_instance.py @@ -111,11 +111,14 @@ def _update(self, data: StageInstancePayload): self.scheduled_event = self.guild.get_scheduled_event(int(event_id)) def __repr__(self) -> str: - return f"" + return ( + "" + ) @cached_slot_property("_cs_channel") def channel(self) -> StageChannel | None: - """Optional[:class:`StageChannel`]: The channel that stage instance is running in.""" + """The channel that stage instance is running in.""" # the returned channel will always be a StageChannel or None return self._state.get_channel(self.channel_id) # type: ignore diff --git a/discord/state.py b/discord/state.py index ef9d8de7..9ec54817 100644 --- a/discord/state.py +++ b/discord/state.py @@ -214,7 +214,8 @@ def __init__( raise TypeError(f"intents parameter must be Intent not {type(intents)!r}") if not intents.guilds: _log.warning( - "Guilds intent seems to be disabled. This may cause state related issues." + "Guilds intent seems to be disabled. This may cause state related" + " issues." ) self._chunk_guilds: bool = options.get( @@ -232,7 +233,8 @@ def __init__( cache_flags = MemberCacheFlags.from_intents(intents) elif not isinstance(cache_flags, MemberCacheFlags): raise TypeError( - f"member_cache_flags parameter must be MemberCacheFlags not {type(cache_flags)!r}" + "member_cache_flags parameter must be MemberCacheFlags not" + f" {type(cache_flags)!r}" ) else: @@ -552,7 +554,10 @@ async def query_members( return await asyncio.wait_for(request.wait(), timeout=30.0) except asyncio.TimeoutError: _log.warning( - "Timed out waiting for chunks with query %r and limit %d for guild_id %d", + ( + "Timed out waiting for chunks with query %r and limit %d for" + " guild_id %d" + ), query, limit, guild_id, @@ -923,7 +928,10 @@ def parse_channel_pins_update(self, data) -> None: if channel is None: _log.debug( - "CHANNEL_PINS_UPDATE referencing an unknown channel ID: %s. Discarding.", + ( + "CHANNEL_PINS_UPDATE referencing an unknown channel ID: %s." + " Discarding." + ), channel_id, ) return @@ -961,23 +969,28 @@ def parse_thread_create(self, data) -> None: def parse_thread_update(self, data) -> None: guild_id = int(data["guild_id"]) guild = self._get_guild(guild_id) + raw = RawThreadUpdateEvent(data) 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) else: - thread = Thread(guild=guild, state=guild._state, data=data) - guild._add_thread(thread) - self.dispatch("thread_join", thread) + thread = guild.get_thread(raw.thread_id) + if thread is not None: + old = copy.copy(thread) + thread._update(data) + if thread.archived: + guild._remove_thread(thread) + self.dispatch("thread_update", old, thread) + else: + thread = Thread(guild=guild, state=guild._state, data=data) + if not thread.archived: + guild._add_thread(thread) + self.dispatch("thread_join", thread) + raw.thread = thread + self.dispatch("raw_thread_update", raw) def parse_thread_delete(self, data) -> None: guild_id = int(data["guild_id"]) @@ -1076,9 +1089,13 @@ def parse_thread_members_update(self, data) -> None: thread_id = int(data["id"]) thread: Thread | None = guild.get_thread(thread_id) + raw = RawThreadMembersUpdateEvent(data) if thread is None: _log.debug( - "THREAD_MEMBERS_UPDATE referencing an unknown thread ID: %s. Discarding", + ( + "THREAD_MEMBERS_UPDATE referencing an unknown thread ID: %s." + " Discarding" + ), thread_id, ) return @@ -1097,6 +1114,7 @@ def parse_thread_members_update(self, data) -> None: for member_id in removed_member_ids: if member_id != self_id: member = thread._pop_member(member_id) + self.dispatch("raw_thread_member_remove", raw) if member is not None: self.dispatch("thread_member_remove", member) else: @@ -1123,6 +1141,9 @@ def parse_guild_member_add(self, data) -> None: self.dispatch("member_join", member) def parse_guild_member_remove(self, data) -> None: + user = self.store_user(data["user"]) + raw = RawMemberRemoveEvent(data, user) + guild = self._get_guild(int(data["guild_id"])) if guild is not None: try: @@ -1130,9 +1151,9 @@ def parse_guild_member_remove(self, data) -> None: except AttributeError: pass - user_id = int(data["user"]["id"]) - member = guild.get_member(user_id) + member = guild.get_member(user.id) if member is not None: + raw.user = member guild._remove_member(member) # type: ignore self.dispatch("member_remove", member) else: @@ -1140,6 +1161,7 @@ def parse_guild_member_remove(self, data) -> None: "GUILD_MEMBER_REMOVE referencing an unknown guild ID: %s. Discarding.", data["guild_id"], ) + self.dispatch("raw_member_remove", raw) def parse_guild_member_update(self, data) -> None: guild = self._get_guild(int(data["guild_id"])) @@ -1196,7 +1218,10 @@ def parse_guild_stickers_update(self, data) -> None: guild = self._get_guild(int(data["guild_id"])) if guild is None: _log.debug( - "GUILD_STICKERS_UPDATE referencing an unknown guild ID: %s. Discarding.", + ( + "GUILD_STICKERS_UPDATE referencing an unknown guild ID: %s." + " Discarding." + ), data["guild_id"], ) return @@ -1409,7 +1434,10 @@ def parse_guild_scheduled_event_create(self, data) -> None: guild = self._get_guild(int(data["guild_id"])) if guild is None: _log.debug( - "GUILD_SCHEDULED_EVENT_CREATE referencing an unknown guild ID: %s. Discarding.", + ( + "GUILD_SCHEDULED_EVENT_CREATE referencing an unknown guild ID: %s." + " Discarding." + ), data["guild_id"], ) return @@ -1429,7 +1457,10 @@ def parse_guild_scheduled_event_update(self, data) -> None: guild = self._get_guild(int(data["guild_id"])) if guild is None: _log.debug( - "GUILD_SCHEDULED_EVENT_UPDATE referencing an unknown guild ID: %s. Discarding.", + ( + "GUILD_SCHEDULED_EVENT_UPDATE referencing an unknown guild ID: %s." + " Discarding." + ), data["guild_id"], ) return @@ -1450,7 +1481,10 @@ def parse_guild_scheduled_event_delete(self, data) -> None: guild = self._get_guild(int(data["guild_id"])) if guild is None: _log.debug( - "GUILD_SCHEDULED_EVENT_DELETE referencing an unknown guild ID: %s. Discarding.", + ( + "GUILD_SCHEDULED_EVENT_DELETE referencing an unknown guild ID: %s." + " Discarding." + ), data["guild_id"], ) return @@ -1471,7 +1505,10 @@ def parse_guild_scheduled_event_user_add(self, data) -> None: guild = self._get_guild(int(data["guild_id"])) if guild is None: _log.debug( - "GUILD_SCHEDULED_EVENT_USER_ADD referencing an unknown guild ID: %s. Discarding.", + ( + "GUILD_SCHEDULED_EVENT_USER_ADD referencing an unknown guild ID:" + " %s. Discarding." + ), data["guild_id"], ) return @@ -1492,7 +1529,10 @@ def parse_guild_scheduled_event_user_remove(self, data) -> None: guild = self._get_guild(int(data["guild_id"])) if guild is None: _log.debug( - "GUILD_SCHEDULED_EVENT_USER_REMOVE referencing an unknown guild ID: %s. Discarding.", + ( + "GUILD_SCHEDULED_EVENT_USER_REMOVE referencing an unknown guild ID:" + " %s. Discarding." + ), data["guild_id"], ) return @@ -1515,7 +1555,10 @@ def parse_guild_integrations_update(self, data) -> None: self.dispatch("guild_integrations_update", guild) else: _log.debug( - "GUILD_INTEGRATIONS_UPDATE referencing an unknown guild ID: %s. Discarding.", + ( + "GUILD_INTEGRATIONS_UPDATE referencing an unknown guild ID: %s." + " Discarding." + ), data["guild_id"], ) @@ -1599,7 +1642,10 @@ def parse_stage_instance_update(self, data) -> None: ) else: _log.debug( - "STAGE_INSTANCE_UPDATE referencing unknown stage instance ID: %s. Discarding.", + ( + "STAGE_INSTANCE_UPDATE referencing unknown stage instance ID:" + " %s. Discarding." + ), data["id"], ) else: @@ -1656,7 +1702,10 @@ def parse_voice_state_update(self, data) -> None: self.dispatch("voice_state_update", member, before, after) else: _log.debug( - "VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.", + ( + "VOICE_STATE_UPDATE referencing an unknown member ID: %s." + " Discarding." + ), data["user_id"], ) @@ -1817,7 +1866,10 @@ async def _delay_ready(self) -> None: else: if self._guild_needs_chunking(guild): _log.debug( - "Guild ID %d requires chunking, will be done in the background.", + ( + "Guild ID %d requires chunking, will be done in the" + " background." + ), guild.id, ) if len(current_bucket) >= max_concurrency: @@ -1826,7 +1878,10 @@ async def _delay_ready(self) -> None: current_bucket, timeout=max_concurrency * 70.0 ) except asyncio.TimeoutError: - fmt = "Shard ID %s failed to wait for chunks from a sub-bucket with length %d" + fmt = ( + "Shard ID %s failed to wait for chunks from a" + " sub-bucket with length %d" + ) _log.warning(fmt, guild.shard_id, len(current_bucket)) finally: current_bucket = [] @@ -1849,7 +1904,10 @@ async def _delay_ready(self) -> None: await utils.sane_wait_for(futures, timeout=timeout) except asyncio.TimeoutError: _log.warning( - "Shard ID %s failed to wait for chunks (timeout=%.2f) for %d guilds", + ( + "Shard ID %s failed to wait for chunks (timeout=%.2f) for %d" + " guilds" + ), shard_id, timeout, len(guilds), diff --git a/discord/sticker.py b/discord/sticker.py index 8a9fe92a..fa5e75ea 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -125,11 +125,14 @@ def _from_data(self, data: StickerPackPayload) -> None: @property def banner(self) -> Asset: - """:class:`Asset`: The banner asset of the sticker pack.""" + """The banner asset of the sticker pack.""" return Asset._from_sticker_banner(self._state, self._banner) def __repr__(self) -> str: - return f"" + return ( + "" + ) def __str__(self) -> str: return self.name @@ -295,7 +298,7 @@ def __str__(self) -> str: @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the sticker's creation time in UTC.""" + """Returns the sticker's creation time in UTC.""" return snowflake_time(self.id) @@ -434,11 +437,14 @@ def _from_data(self, data: GuildStickerPayload) -> None: self.type: StickerType = StickerType.guild def __repr__(self) -> str: - return f"" + return ( + "" + ) @cached_slot_property("_cs_guild") def guild(self) -> Guild | None: - """Optional[:class:`Guild`]: The guild that this sticker is from. + """The guild that this sticker is from. Could be ``None`` if the bot is not in the guild. .. versionadded:: 2.0 diff --git a/discord/team.py b/discord/team.py index a0548367..10432a4d 100644 --- a/discord/team.py +++ b/discord/team.py @@ -78,14 +78,14 @@ def __repr__(self) -> str: @property def icon(self) -> Asset | None: - """Optional[:class:`.Asset`]: Retrieves the team's icon asset, if any.""" + """Retrieves the team's icon asset, if any.""" if self._icon is None: return None return Asset._from_icon(self._state, self.id, self._icon, path="team") @property def owner(self) -> TeamMember | None: - """Optional[:class:`TeamMember`]: The team's owner.""" + """The team's owner.""" return utils.get(self.members, id=self.owner_id) diff --git a/discord/template.py b/discord/template.py index c13b70d1..74455a17 100644 --- a/discord/template.py +++ b/discord/template.py @@ -307,7 +307,7 @@ async def delete(self) -> None: @property def url(self) -> str: - """:class:`str`: The template url. + """The template url. .. versionadded:: 2.0 """ diff --git a/discord/threads.py b/discord/threads.py index cf34e854..6eb1bfc0 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -41,7 +41,7 @@ if TYPE_CHECKING: from .abc import Snowflake, SnowflakeTime - from .channel import CategoryChannel, ForumChannel, TextChannel + from .channel import CategoryChannel, ForumChannel, ForumTag, TextChannel from .guild import Guild from .member import Member from .message import Message, PartialMessage @@ -128,6 +128,12 @@ class Thread(Messageable, Hashable): Extra features of the thread. .. versionadded:: 2.0 + total_message_sent: :class:`int` + Number of messages ever sent in a thread. + It's similar to message_count on message creation, + but will not decrement the number when a message is deleted. + + .. versionadded:: 2.3 """ __slots__ = ( @@ -137,6 +143,7 @@ class Thread(Messageable, Hashable): "_type", "_state", "_members", + "_applied_tags", "owner_id", "parent_id", "last_message_id", @@ -151,6 +158,7 @@ class Thread(Messageable, Hashable): "archive_timestamp", "created_at", "flags", + "total_message_sent", ) def __init__(self, *, guild: Guild, state: ConnectionState, data: ThreadPayload): @@ -189,6 +197,10 @@ def _from_data(self, data: ThreadPayload): self.message_count = data.get("message_count", None) self.member_count = data.get("member_count", None) self.flags: ChannelFlags = ChannelFlags._from_value(data.get("flags", 0)) + self.total_message_sent = data.get("total_message_sent", None) + self._applied_tags: list[int] = [ + int(tag_id) for tag_id in data.get("applied_tags", []) + ] # Here, we try to fill in potentially missing data if thread := self.guild.get_thread(self.id) and data.pop("_invoke_flag", False): @@ -203,6 +215,11 @@ def _from_data(self, data: ThreadPayload): if self.message_count is None else self.message_count ) + self.total_message_sent = ( + thread.total_message_sent + if self.total_message_sent is None + else self.total_message_sent + ) self.member_count = ( thread.member_count if self.member_count is None else self.member_count ) @@ -240,27 +257,27 @@ def _update(self, data): @property def type(self) -> ChannelType: - """:class:`ChannelType`: The channel's Discord type.""" + """The channel's Discord type.""" return self._type @property def parent(self) -> TextChannel | ForumChannel | None: - """Optional[:class:`TextChannel`]: The parent channel this thread belongs to.""" + """The parent channel this thread belongs to.""" return self.guild.get_channel(self.parent_id) # type: ignore @property def owner(self) -> Member | None: - """Optional[:class:`Member`]: The member this thread belongs to.""" + """The member this thread belongs to.""" return self.guild.get_member(self.owner_id) @property def mention(self) -> str: - """:class:`str`: The string that allows you to mention the thread.""" + """The string that allows you to mention the thread.""" return f"<#{self.id}>" @property def jump_url(self) -> str: - """:class:`str`: Returns a URL that allows the client to jump to the thread. + """Returns a URL that allows the client to jump to the thread. .. versionadded:: 2.0 """ @@ -268,7 +285,7 @@ def jump_url(self) -> str: @property def members(self) -> list[ThreadMember]: - """List[:class:`ThreadMember`]: A list of thread members in this thread. + """A list of thread members in this thread. This requires :attr:`Intents.members` to be properly filled. Most of the time however, this data is not provided by the gateway and a call to :meth:`fetch_members` is @@ -276,6 +293,22 @@ def members(self) -> list[ThreadMember]: """ return list(self._members.values()) + @property + def applied_tags(self) -> list[ForumTag]: + """List[:class:`ForumTag`]: A list of tags applied to this thread. + + This is only available for threads in forum channels. + """ + from .channel import ForumChannel # to prevent circular import + + if isinstance(self.parent, ForumChannel): + return [ + tag + for tag_id in self._applied_tags + if (tag := self.parent.get_tag(tag_id)) is not None + ] + return [] + @property def last_message(self) -> Message | None: """Returns the last message from this thread in cache. @@ -357,8 +390,15 @@ def starting_message(self) -> Message | None: """ return self._state._get_message(self.id) + def is_pinned(self) -> bool: + """Whether the thread is pinned to the top of its parent forum channel. + + .. versionadded:: 2.3 + """ + return self.flags.pinned + def is_private(self) -> bool: - """:class:`bool`: Whether the thread is a private thread. + """Whether the thread is a private thread. A private thread is only viewable by those that have been explicitly invited or have :attr:`~.Permissions.manage_threads`. @@ -366,7 +406,7 @@ def is_private(self) -> bool: return self._type is ChannelType.private_thread def is_news(self) -> bool: - """:class:`bool`: Whether the thread is a news thread. + """Whether the thread is a news thread. A news thread is a thread that has a parent that is a news channel, i.e. :meth:`.TextChannel.is_news` is ``True``. @@ -374,7 +414,7 @@ def is_news(self) -> bool: return self._type is ChannelType.news_thread def is_nsfw(self) -> bool: - """:class:`bool`: Whether the thread is NSFW or not. + """Whether the thread is NSFW or not. An NSFW thread is a thread that has a parent that is an NSFW channel, i.e. :meth:`.TextChannel.is_nsfw` is ``True``. @@ -561,6 +601,7 @@ async def edit( slowmode_delay: int = MISSING, auto_archive_duration: ThreadArchiveDuration = MISSING, pinned: bool = MISSING, + applied_tags: list[ForumTag] = MISSING, reason: str | None = None, ) -> Thread: """|coro| @@ -595,6 +636,10 @@ async def edit( The reason for editing this thread. Shows up on the audit log. pinned: :class:`bool` Whether to pin the thread or not. This only works if the thread is part of a forum. + applied_tags: List[:class:`ForumTag`] + The set of tags to apply to the thread. Each tag object should have an ID set. + + .. versionadded:: 2.3 Returns ------- @@ -626,6 +671,8 @@ async def edit( flags = ChannelFlags._from_value(self.flags.value) flags.pinned = pinned payload["flags"] = flags.value + if applied_tags is not MISSING: + payload["applied_tags"] = [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 @@ -856,7 +903,10 @@ def __init__(self, parent: Thread, data: ThreadMemberPayload): self._from_data(data) def __repr__(self) -> str: - return f"" + return ( + "" + ) def _from_data(self, data: ThreadMemberPayload): try: @@ -875,5 +925,5 @@ def _from_data(self, data: ThreadMemberPayload): @property def thread(self) -> Thread: - """:class:`Thread`: The thread this member belongs to.""" + """The thread this member belongs to.""" return self.parent diff --git a/discord/types/activity.py b/discord/types/activity.py index af959ef5..3c610e31 100644 --- a/discord/types/activity.py +++ b/discord/types/activity.py @@ -25,8 +25,9 @@ from __future__ import annotations -from typing import Literal, TypedDict +from typing import Literal +from .._typed_dict import NotRequired, TypedDict from .snowflake import Snowflake from .user import PartialUser @@ -70,12 +71,9 @@ class ActivitySecrets(TypedDict, total=False): match: str -class _ActivityEmojiOptional(TypedDict, total=False): - id: Snowflake - animated: bool - - -class ActivityEmoji(_ActivityEmojiOptional): +class ActivityEmoji(TypedDict): + id: NotRequired[Snowflake] + animated: NotRequired[bool] name: str @@ -84,14 +82,11 @@ class ActivityButton(TypedDict): url: str -class _SendableActivityOptional(TypedDict, total=False): - url: str | None - - ActivityType = Literal[0, 1, 2, 4, 5] -class SendableActivity(_SendableActivityOptional): +class SendableActivity(TypedDict): + url: NotRequired[str | None] name: str type: ActivityType diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index f11db4fd..989d3f7a 100644 --- a/discord/types/appinfo.py +++ b/discord/types/appinfo.py @@ -25,8 +25,7 @@ from __future__ import annotations -from typing import TypedDict - +from .._typed_dict import NotRequired, TypedDict from .snowflake import Snowflake from .team import Team from .user import User @@ -39,35 +38,24 @@ class BaseAppInfo(TypedDict): icon: str | None summary: str description: str + terms_of_service_url: NotRequired[str] + privacy_policy_url: NotRequired[str] + hook: NotRequired[bool] + max_participants: NotRequired[int] -class _AppInfoOptional(TypedDict, total=False): - team: Team - guild_id: Snowflake - primary_sku_id: Snowflake - slug: str - terms_of_service_url: str - privacy_policy_url: str - hook: bool - max_participants: int - - -class AppInfo(BaseAppInfo, _AppInfoOptional): +class AppInfo(BaseAppInfo): + team: NotRequired[Team] + guild_id: NotRequired[Snowflake] + primary_sku_id: NotRequired[Snowflake] + slug: NotRequired[str] rpc_origins: list[str] owner: User bot_public: bool bot_require_code_grant: bool -class _PartialAppInfoOptional(TypedDict, total=False): - rpc_origins: list[str] - cover_image: str - hook: bool - terms_of_service_url: str - privacy_policy_url: str - max_participants: int - flags: int - - -class PartialAppInfo(_PartialAppInfoOptional, BaseAppInfo): - pass +class PartialAppInfo(BaseAppInfo): + rpc_origins: NotRequired[list[str]] + cover_image: NotRequired[str] + flags: NotRequired[int] diff --git a/discord/types/application_role_connection.py b/discord/types/application_role_connection.py new file mode 100644 index 00000000..40bcae10 --- /dev/null +++ b/discord/types/application_role_connection.py @@ -0,0 +1,40 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +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 Literal + +from .._typed_dict import NotRequired, TypedDict + +ApplicationRoleConnectionMetadataType = Literal[1, 2, 3, 4, 5, 6, 7, 8] + + +class ApplicationRoleConnectionMetadata(TypedDict): + type: ApplicationRoleConnectionMetadataType + key: str + name: str + name_localizations: NotRequired[dict[str, str]] + description: str + description_localizations: NotRequired[dict[str, str]] diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py index 615b4325..7e00526e 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -25,8 +25,9 @@ from __future__ import annotations -from typing import Literal, TypedDict, Union +from typing import Literal, Union +from .._typed_dict import NotRequired, TypedDict from .automod import AutoModRule from .channel import ChannelType, PermissionOverwrite, VideoQualityMode from .guild import ( @@ -268,13 +269,10 @@ class AuditEntryInfo(TypedDict): role_name: str -class _AuditLogEntryOptional(TypedDict, total=False): - changes: list[AuditLogChange] - options: AuditEntryInfo - reason: str - - -class AuditLogEntry(_AuditLogEntryOptional): +class AuditLogEntry(TypedDict): + changes: NotRequired[list[AuditLogChange]] + options: NotRequired[AuditEntryInfo] + reason: NotRequired[str] target_id: str | None user_id: Snowflake | None id: Snowflake diff --git a/discord/types/automod.py b/discord/types/automod.py index 16684904..4632c8d8 100644 --- a/discord/types/automod.py +++ b/discord/types/automod.py @@ -22,11 +22,12 @@ from __future__ import annotations -from typing import Literal, TypedDict +from typing import Literal +from .._typed_dict import NotRequired, TypedDict from .snowflake import Snowflake -AutoModTriggerType = Literal[1, 2, 3, 4] +AutoModTriggerType = Literal[1, 2, 3, 4, 5] AutoModEventType = Literal[1] @@ -37,7 +38,10 @@ class AutoModTriggerMetadata(TypedDict, total=False): keyword_filter: list[str] + regex_patterns: list[str] presets: list[AutoModKeywordPresetType] + allow_list: list[str] + mention_total_limit: int class AutoModActionMetadata(TypedDict, total=False): @@ -64,13 +68,10 @@ class AutoModRule(TypedDict): exempt_channels: list[Snowflake] -class _CreateAutoModRuleOptional(TypedDict, total=False): - enabled: bool - exempt_roles: list[Snowflake] - exempt_channels: list[Snowflake] - - -class CreateAutoModRule(_CreateAutoModRuleOptional): +class CreateAutoModRule(TypedDict): + enabled: NotRequired[bool] + exempt_roles: NotRequired[list[Snowflake]] + exempt_channels: NotRequired[list[Snowflake]] name: str event_type: AutoModEventType trigger_type: AutoModTriggerType diff --git a/discord/types/channel.py b/discord/types/channel.py index 2b7c2d3a..bc825a3f 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -22,9 +22,13 @@ 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 List, Literal, Optional, TypedDict, Union +from typing import Literal, Union +from .._typed_dict import NotRequired, TypedDict +from ..enums import SortOrder +from ..flags import ChannelFlags from .snowflake import Snowflake from .threads import ThreadArchiveDuration, ThreadMember, ThreadMetadata from .user import PartialUser @@ -50,9 +54,9 @@ class _BaseChannel(TypedDict): class _BaseGuildChannel(_BaseChannel): guild_id: Snowflake position: int - permission_overwrites: List[PermissionOverwrite] + permission_overwrites: list[PermissionOverwrite] nsfw: bool - parent_id: Optional[Snowflake] + parent_id: Snowflake | None class PartialChannel(_BaseChannel): @@ -61,18 +65,36 @@ class PartialChannel(_BaseChannel): class _TextChannelOptional(TypedDict, total=False): topic: str - last_message_id: Optional[Snowflake] + last_message_id: Snowflake | None last_pin_timestamp: str rate_limit_per_user: int default_auto_archive_duration: ThreadArchiveDuration + default_thread_rate_limit_per_user: int class TextChannel(_BaseGuildChannel, _TextChannelOptional): type: Literal[0] +class DefaultReaction(TypedDict): + emoji_id: NotRequired[Snowflake | None] + emoji_name: NotRequired[str | None] + + +class ForumTag(TypedDict): + id: Snowflake + name: str + moderated: bool + emoji_id: NotRequired[Snowflake | None] + emoji_name: NotRequired[str | None] + + class ForumChannel(_BaseGuildChannel, _TextChannelOptional): type: Literal[15] + available_tags: NotRequired[list[ForumTag] | None] + default_reaction_emoji: NotRequired[DefaultReaction | None] + default_sort_order: NotRequired[SortOrder | None] + flags: ChannelFlags class NewsChannel(_BaseGuildChannel, _TextChannelOptional): @@ -82,12 +104,9 @@ class NewsChannel(_BaseGuildChannel, _TextChannelOptional): VideoQualityMode = Literal[1, 2] -class _VoiceChannelOptional(TypedDict, total=False): - rtc_region: Optional[str] - video_quality_mode: VideoQualityMode - - -class VoiceChannel(_BaseGuildChannel, _VoiceChannelOptional): +class VoiceChannel(_BaseGuildChannel): + rtc_region: NotRequired[str | None] + video_quality_mode: NotRequired[VideoQualityMode] type: Literal[2] bitrate: int user_limit: int @@ -97,36 +116,30 @@ class CategoryChannel(_BaseGuildChannel): type: Literal[4] -class _StageChannelOptional(TypedDict, total=False): - rtc_region: Optional[str] - topic: str - - -class StageChannel(_BaseGuildChannel, _StageChannelOptional): +class StageChannel(_BaseGuildChannel): + rtc_region: NotRequired[str | None] + topic: NotRequired[str] type: Literal[13] bitrate: int user_limit: int -class _ThreadChannelOptional(TypedDict, total=False): - member: ThreadMember - owner_id: Snowflake - rate_limit_per_user: int - last_message_id: Optional[Snowflake] - last_pin_timestamp: str - - -class ThreadChannel(_BaseChannel, _ThreadChannelOptional): +class ThreadChannel(_BaseChannel): + member: NotRequired[ThreadMember] + owner_id: NotRequired[Snowflake] + rate_limit_per_user: NotRequired[int] + last_message_id: NotRequired[Snowflake | None] + last_pin_timestamp: NotRequired[str] type: Literal[10, 11, 12] guild_id: Snowflake parent_id: Snowflake - owner_id: Snowflake nsfw: bool - last_message_id: Optional[Snowflake] - rate_limit_per_user: int message_count: int member_count: int thread_metadata: ThreadMetadata + applied_tags: NotRequired[list[Snowflake] | None] + flags: ChannelFlags + total_message_sent: int GuildChannel = Union[ @@ -143,13 +156,13 @@ class ThreadChannel(_BaseChannel, _ThreadChannelOptional): class DMChannel(TypedDict): id: Snowflake type: Literal[1] - last_message_id: Optional[Snowflake] - recipients: List[PartialUser] + last_message_id: Snowflake | None + recipients: list[PartialUser] class GroupDMChannel(_BaseChannel): type: Literal[3] - icon: Optional[str] + icon: str | None owner_id: Snowflake diff --git a/discord/types/components.py b/discord/types/components.py index 4a9b4aa9..fa82d47b 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -25,8 +25,10 @@ from __future__ import annotations -from typing import Literal, TypedDict, Union +from typing import Literal, Union +from .._typed_dict import NotRequired, TypedDict +from .channel import ChannelType from .emoji import PartialEmoji ComponentType = Literal[1, 2, 3, 4] @@ -39,56 +41,45 @@ class ActionRow(TypedDict): components: list[Component] -class _ButtonComponentOptional(TypedDict, total=False): - custom_id: str - url: str - disabled: bool - emoji: PartialEmoji - label: str - - -class ButtonComponent(_ButtonComponentOptional): +class ButtonComponent(TypedDict): + custom_id: NotRequired[str] + url: NotRequired[str] + disabled: NotRequired[bool] + emoji: NotRequired[PartialEmoji] + label: NotRequired[str] type: Literal[2] style: ButtonStyle -class _InputTextComponentOptional(TypedDict, total=False): - min_length: int - max_length: int - required: bool - placeholder: str - value: str - - -class InputText(_InputTextComponentOptional): +class InputText(TypedDict): + min_length: NotRequired[int] + max_length: NotRequired[int] + required: NotRequired[bool] + placeholder: NotRequired[str] + value: NotRequired[str] type: Literal[4] style: InputTextStyle custom_id: str label: str -class _SelectMenuOptional(TypedDict, total=False): - placeholder: str - min_values: int - max_values: int - disabled: bool - - -class _SelectOptionsOptional(TypedDict, total=False): - description: str - emoji: PartialEmoji - - -class SelectOption(_SelectOptionsOptional): +class SelectOption(TypedDict): + description: NotRequired[str] + emoji: NotRequired[PartialEmoji] label: str value: str default: bool -class SelectMenu(_SelectMenuOptional): - type: Literal[3] +class SelectMenu(TypedDict): + placeholder: NotRequired[str] + min_values: NotRequired[int] + max_values: NotRequired[int] + disabled: NotRequired[bool] + channel_types: NotRequired[list[ChannelType]] + options: NotRequired[list[SelectOption]] + type: Literal[3, 5, 6, 7, 8] custom_id: str - options: list[SelectOption] Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText] diff --git a/discord/types/embed.py b/discord/types/embed.py index f4de616a..0d73afe4 100644 --- a/discord/types/embed.py +++ b/discord/types/embed.py @@ -22,24 +22,21 @@ 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 List, Literal, TypedDict +from typing import Literal - -class _EmbedFooterOptional(TypedDict, total=False): - icon_url: str - proxy_icon_url: str +from .._typed_dict import NotRequired, TypedDict -class EmbedFooter(_EmbedFooterOptional): +class EmbedFooter(TypedDict): + icon_url: NotRequired[str] + proxy_icon_url: NotRequired[str] text: str -class _EmbedFieldOptional(TypedDict, total=False): - inline: bool - - -class EmbedField(_EmbedFieldOptional): +class EmbedField(TypedDict): + inline: NotRequired[bool] name: str value: str @@ -95,4 +92,4 @@ class Embed(TypedDict, total=False): video: EmbedVideo provider: EmbedProvider author: EmbedAuthor - fields: List[EmbedField] + fields: list[EmbedField] diff --git a/discord/types/emoji.py b/discord/types/emoji.py index 83289754..68040ebc 100644 --- a/discord/types/emoji.py +++ b/discord/types/emoji.py @@ -22,16 +22,17 @@ 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, TypedDict +from typing import TypedDict from .snowflake import Snowflake, SnowflakeList from .user import User class PartialEmoji(TypedDict): - id: Optional[Snowflake] - name: Optional[str] + id: Snowflake | None + name: str | None class Emoji(PartialEmoji, total=False): @@ -45,4 +46,4 @@ class Emoji(PartialEmoji, total=False): class EditEmoji(TypedDict): name: str - roles: Optional[SnowflakeList] + roles: SnowflakeList | None diff --git a/discord/types/guild.py b/discord/types/guild.py index b5b39dd8..567f36d5 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -22,9 +22,11 @@ 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 List, Literal, Optional, TypedDict +from typing import Literal +from .._typed_dict import NotRequired, Required, TypedDict from .activity import PartialPresenceUpdate from .channel import GuildChannel from .emoji import Emoji @@ -39,40 +41,15 @@ class Ban(TypedDict): - reason: Optional[str] + reason: str | None user: User -class _UnavailableGuildOptional(TypedDict, total=False): - unavailable: bool - - -class UnavailableGuild(_UnavailableGuildOptional): +class UnavailableGuild(TypedDict): + unavailable: NotRequired[bool] id: Snowflake -class _GuildOptional(TypedDict, total=False): - icon_hash: Optional[str] - owner: bool - permissions: str - widget_enabled: bool - widget_channel_id: Optional[Snowflake] - joined_at: Optional[str] - large: bool - member_count: int - voice_states: List[GuildVoiceState] - members: List[Member] - channels: List[GuildChannel] - presences: List[PartialPresenceUpdate] - threads: List[Thread] - max_presences: Optional[int] - max_members: int - premium_subscription_count: int - premium_progress_bar_enabled: bool - max_video_channel_users: int - guild_scheduled_events: List[ScheduledEvent] - - DefaultMessageNotificationLevel = Literal[0, 1] ExplicitContentFilterLevel = Literal[0, 1, 2] MFALevel = Literal[0, 1] @@ -82,14 +59,19 @@ class _GuildOptional(TypedDict, total=False): GuildFeature = Literal[ "ANIMATED_BANNER", "ANIMATED_ICON", + "APPLICATION_COMMAND_PERMISSIONS_V2", "AUTO_MODERATION", "BANNER", + "CHANNEL_BANNER", "COMMERCE", "COMMUNITY", + "DEVELOPER_SUPPORT_SERVER", "DISCOVERABLE", "FEATURABLE", "HAS_DIRECTORY_ENTRY", "HUB", + "INTERNAL_EMPLOYEE_ONLY", + "INVITES_DISABLED", "INVITE_SPLASH", "LINKED_TO_HUB", "MEMBER_PROFILES", @@ -102,7 +84,6 @@ class _GuildOptional(TypedDict, total=False): "PARTNERED", "PREMIUM_TIER_3_OVERRIDE", "PREVIEW_ENABLED", - "PRIVATE_THREADS", "ROLE_ICONS", "ROLE_SUBSCRIPTIONS_ENABLED", "SEVEN_DAY_THREAD_ARCHIVE", @@ -120,12 +101,12 @@ class _GuildOptional(TypedDict, total=False): class _BaseGuildPreview(UnavailableGuild): name: str - icon: Optional[str] - splash: Optional[str] - discovery_splash: Optional[str] - emojis: List[Emoji] - features: List[GuildFeature] - description: Optional[str] + icon: str | None + splash: str | None + discovery_splash: str | None + emojis: list[Emoji] + features: list[GuildFeature] + description: str | None class _GuildPreviewUnique(TypedDict): @@ -137,25 +118,44 @@ class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique): pass -class Guild(_BaseGuildPreview, _GuildOptional): +class Guild(_BaseGuildPreview): + icon_hash: NotRequired[str | None] + owner: NotRequired[bool] + permissions: NotRequired[str] + widget_enabled: NotRequired[bool] + widget_channel_id: NotRequired[Snowflake | None] + joined_at: NotRequired[str | None] + large: NotRequired[bool] + member_count: NotRequired[int] + voice_states: NotRequired[list[GuildVoiceState]] + members: NotRequired[list[Member]] + channels: NotRequired[list[GuildChannel]] + presences: NotRequired[list[PartialPresenceUpdate]] + threads: NotRequired[list[Thread]] + max_presences: NotRequired[int | None] + max_members: NotRequired[int] + premium_subscription_count: NotRequired[int] + premium_progress_bar_enabled: NotRequired[bool] + max_video_channel_users: NotRequired[int] + guild_scheduled_events: NotRequired[list[ScheduledEvent]] owner_id: Snowflake - afk_channel_id: Optional[Snowflake] + afk_channel_id: Snowflake | None afk_timeout: int verification_level: VerificationLevel default_message_notifications: DefaultMessageNotificationLevel explicit_content_filter: ExplicitContentFilterLevel - roles: List[Role] + roles: list[Role] mfa_level: MFALevel nsfw_level: NSFWLevel - application_id: Optional[Snowflake] - system_channel_id: Optional[Snowflake] + application_id: Snowflake | None + system_channel_id: Snowflake | None system_channel_flags: int - rules_channel_id: Optional[Snowflake] - vanity_url_code: Optional[str] - banner: Optional[str] + rules_channel_id: Snowflake | None + vanity_url_code: str | None + banner: str | None premium_tier: PremiumTier preferred_locale: str - public_updates_channel_id: Optional[Snowflake] + public_updates_channel_id: Snowflake | None class InviteGuild(Guild, total=False): @@ -167,22 +167,19 @@ class GuildWithCounts(Guild, _GuildPreviewUnique): class GuildPrune(TypedDict): - pruned: Optional[int] + pruned: int | None class ChannelPositionUpdate(TypedDict): id: Snowflake - position: Optional[int] - lock_permissions: Optional[bool] - parent_id: Optional[Snowflake] - - -class _RolePositionRequired(TypedDict): - id: Snowflake + position: int | None + lock_permissions: bool | None + parent_id: Snowflake | None -class RolePositionUpdate(_RolePositionRequired, total=False): - position: Optional[Snowflake] +class RolePositionUpdate(TypedDict, total=False): + id: Required[Snowflake] + position: Snowflake | None class GuildMFAModify(TypedDict): diff --git a/discord/types/integration.py b/discord/types/integration.py index 4eda85a6..219992d2 100644 --- a/discord/types/integration.py +++ b/discord/types/integration.py @@ -25,17 +25,15 @@ from __future__ import annotations -from typing import Literal, TypedDict, Union +from typing import Literal, Union +from .._typed_dict import NotRequired, TypedDict from .snowflake import Snowflake from .user import User -class _IntegrationApplicationOptional(TypedDict, total=False): - bot: User - - -class IntegrationApplication(_IntegrationApplicationOptional): +class IntegrationApplication(TypedDict): + bot: NotRequired[User] id: Snowflake name: str icon: str | None diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 292580b6..45824770 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -25,7 +25,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, TypedDict, Union +from typing import TYPE_CHECKING, Literal, Union from ..permissions import Permissions from .channel import ChannelType @@ -40,48 +40,40 @@ if TYPE_CHECKING: from .message import AllowedMentions, Message +from .._typed_dict import NotRequired, TypedDict ApplicationCommandType = Literal[1, 2, 3] -class _ApplicationCommandOptional(TypedDict, total=False): - options: list[ApplicationCommandOption] - type: ApplicationCommandType - name_localized: str - name_localizations: dict[str, str] - description_localized: str - description_localizations: dict[str, str] - - -class ApplicationCommand(_ApplicationCommandOptional): +class ApplicationCommand(TypedDict): + options: NotRequired[list[ApplicationCommandOption]] + type: NotRequired[ApplicationCommandType] + name_localized: NotRequired[str] + name_localizations: NotRequired[dict[str, str]] + description_localized: NotRequired[str] + description_localizations: NotRequired[dict[str, str]] id: Snowflake application_id: Snowflake name: str description: str -class _ApplicationCommandOptionOptional(TypedDict, total=False): - choices: list[ApplicationCommandOptionChoice] - options: list[ApplicationCommandOption] - name_localizations: dict[str, str] - description_localizations: dict[str, str] - - ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] -class ApplicationCommandOption(_ApplicationCommandOptionOptional): +class ApplicationCommandOption(TypedDict): + choices: NotRequired[list[ApplicationCommandOptionChoice]] + options: NotRequired[list[ApplicationCommandOption]] + name_localizations: NotRequired[dict[str, str]] + description_localizations: NotRequired[dict[str, str]] type: ApplicationCommandOptionType name: str description: str required: bool -class _ApplicationCommandOptionChoiceOptional(TypedDict, total=False): - name_localizations: dict[str, str] - - -class ApplicationCommandOptionChoice(_ApplicationCommandOptionChoiceOptional): +class ApplicationCommandOptionChoice(TypedDict): + name_localizations: NotRequired[dict[str, str]] name: str value: str | int @@ -182,23 +174,17 @@ class ApplicationCommandInteractionDataResolved(TypedDict, total=False): attachments: dict[Snowflake, Attachment] -class _ApplicationCommandInteractionDataOptional(TypedDict, total=False): - options: list[ApplicationCommandInteractionDataOption] - resolved: ApplicationCommandInteractionDataResolved - target_id: Snowflake - type: ApplicationCommandType - - -class ApplicationCommandInteractionData(_ApplicationCommandInteractionDataOptional): +class ApplicationCommandInteractionData(TypedDict): + options: NotRequired[list[ApplicationCommandInteractionDataOption]] + resolved: NotRequired[ApplicationCommandInteractionDataResolved] + target_id: NotRequired[Snowflake] + type: NotRequired[ApplicationCommandType] id: Snowflake name: str -class _ComponentInteractionDataOptional(TypedDict, total=False): - values: list[str] - - -class ComponentInteractionData(_ComponentInteractionDataOptional): +class ComponentInteractionData(TypedDict): + values: NotRequired[list[str]] custom_id: str component_type: ComponentType @@ -206,19 +192,16 @@ class ComponentInteractionData(_ComponentInteractionDataOptional): InteractionData = Union[ApplicationCommandInteractionData, ComponentInteractionData] -class _InteractionOptional(TypedDict, total=False): - data: InteractionData - guild_id: Snowflake - channel_id: Snowflake - member: Member - user: User - message: Message - locale: str - guild_locale: str - app_permissions: Permissions - - -class Interaction(_InteractionOptional): +class Interaction(TypedDict): + data: NotRequired[InteractionData] + guild_id: NotRequired[Snowflake] + channel_id: NotRequired[Snowflake] + member: NotRequired[Member] + user: NotRequired[User] + message: NotRequired[Message] + locale: NotRequired[str] + guild_locale: NotRequired[str] + app_permissions: NotRequired[Permissions] id: Snowflake application_id: Snowflake type: InteractionType @@ -238,11 +221,8 @@ class InteractionApplicationCommandCallbackData(TypedDict, total=False): InteractionResponseType = Literal[1, 4, 5, 6, 7] -class _InteractionResponseOptional(TypedDict, total=False): - data: InteractionApplicationCommandCallbackData - - -class InteractionResponse(_InteractionResponseOptional): +class InteractionResponse(TypedDict): + data: NotRequired[InteractionApplicationCommandCallbackData] type: InteractionResponseType @@ -253,12 +233,9 @@ class MessageInteraction(TypedDict): user: User -class _EditApplicationCommandOptional(TypedDict, total=False): - description: str - options: list[ApplicationCommandOption] | None - type: ApplicationCommandType - - -class EditApplicationCommand(_EditApplicationCommandOptional): +class EditApplicationCommand(TypedDict): + description: NotRequired[str] + options: NotRequired[list[ApplicationCommandOption] | None] + type: NotRequired[ApplicationCommandType] name: str default_permission: bool diff --git a/discord/types/invite.py b/discord/types/invite.py index 575a58bf..7f1e23b9 100644 --- a/discord/types/invite.py +++ b/discord/types/invite.py @@ -25,8 +25,9 @@ from __future__ import annotations -from typing import Literal, TypedDict, Union +from typing import Literal, Union +from .._typed_dict import NotRequired, TypedDict from .appinfo import PartialAppInfo from .channel import PartialChannel from .guild import InviteGuild, _GuildPreviewUnique @@ -37,15 +38,6 @@ InviteTargetType = Literal[1, 2] -class _InviteOptional(TypedDict, total=False): - guild: InviteGuild - inviter: PartialUser - scheduled_event: ScheduledEvent - target_user: PartialUser - target_type: InviteTargetType - target_application: PartialAppInfo - - class _InviteMetadata(TypedDict, total=False): uses: int max_uses: int @@ -64,23 +56,25 @@ class IncompleteInvite(_InviteMetadata): channel: PartialChannel -class Invite(IncompleteInvite, _InviteOptional): - pass +class Invite(IncompleteInvite): + guild: NotRequired[InviteGuild] + inviter: NotRequired[PartialUser] + scheduled_event: NotRequired[ScheduledEvent] + target_user: NotRequired[PartialUser] + target_type: NotRequired[InviteTargetType] + target_application: NotRequired[PartialAppInfo] class InviteWithCounts(Invite, _GuildPreviewUnique): pass -class _GatewayInviteCreateOptional(TypedDict, total=False): - guild_id: Snowflake - inviter: PartialUser - target_type: InviteTargetType - target_user: PartialUser - target_application: PartialAppInfo - - -class GatewayInviteCreate(_GatewayInviteCreateOptional): +class GatewayInviteCreate(TypedDict): + guild_id: NotRequired[Snowflake] + inviter: NotRequired[PartialUser] + target_type: NotRequired[InviteTargetType] + target_user: NotRequired[PartialUser] + target_application: NotRequired[PartialAppInfo] channel_id: Snowflake code: str created_at: str @@ -90,11 +84,8 @@ class GatewayInviteCreate(_GatewayInviteCreateOptional): uses: bool -class _GatewayInviteDeleteOptional(TypedDict, total=False): - guild_id: Snowflake - - -class GatewayInviteDelete(_GatewayInviteDeleteOptional): +class GatewayInviteDelete(TypedDict): + guild_id: NotRequired[Snowflake] channel_id: Snowflake code: str diff --git a/discord/types/message.py b/discord/types/message.py index 940f3ae1..4d6d295d 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -25,7 +25,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, TypedDict +from typing import TYPE_CHECKING, Literal from .channel import ChannelType from .components import Component @@ -40,6 +40,8 @@ if TYPE_CHECKING: from .interactions import MessageInteraction +from .._typed_dict import NotRequired, TypedDict + class ChannelMention(TypedDict): id: Snowflake @@ -54,14 +56,11 @@ class Reaction(TypedDict): emoji: PartialEmoji -class _AttachmentOptional(TypedDict, total=False): - height: int | None - width: int | None - content_type: str - spoiler: bool - - -class Attachment(_AttachmentOptional): +class Attachment(TypedDict): + height: NotRequired[int | None] + width: NotRequired[int | None] + content_type: NotRequired[str] + spoiler: NotRequired[bool] id: Snowflake filename: str size: int @@ -77,11 +76,8 @@ class MessageActivity(TypedDict): party_id: str -class _MessageApplicationOptional(TypedDict, total=False): - cover_image: str - - -class MessageApplication(_MessageApplicationOptional): +class MessageApplication(TypedDict): + cover_image: NotRequired[str] id: Snowflake description: str icon: str | None @@ -95,31 +91,28 @@ class MessageReference(TypedDict, total=False): fail_if_not_exists: bool -class _MessageOptional(TypedDict, total=False): - guild_id: Snowflake - member: Member - mention_channels: list[ChannelMention] - reactions: list[Reaction] - nonce: int | str - webhook_id: Snowflake - activity: MessageActivity - application: MessageApplication - application_id: Snowflake - message_reference: MessageReference - flags: int - sticker_items: list[StickerItem] - referenced_message: Message | None - interaction: MessageInteraction - components: list[Component] - thread: Thread | None - - MessageType = Literal[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 19, 20, 21, 22, 23, 24 ] -class Message(_MessageOptional): +class Message(TypedDict): + guild_id: NotRequired[Snowflake] + member: NotRequired[Member] + mention_channels: NotRequired[list[ChannelMention]] + reactions: NotRequired[list[Reaction]] + nonce: NotRequired[int | str] + webhook_id: NotRequired[Snowflake] + activity: NotRequired[MessageActivity] + application: NotRequired[MessageApplication] + application_id: NotRequired[Snowflake] + message_reference: NotRequired[MessageReference] + flags: NotRequired[int] + sticker_items: NotRequired[list[StickerItem]] + referenced_message: NotRequired[Message | None] + interaction: NotRequired[MessageInteraction] + components: NotRequired[list[Component]] + thread: NotRequired[Thread | None] id: Snowflake channel_id: Snowflake author: User diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index 069a1e90..930ed475 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.py @@ -22,13 +22,15 @@ 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 List, TypedDict - +from .._typed_dict import NotRequired, TypedDict from .automod import AutoModAction, AutoModTriggerType from .emoji import PartialEmoji from .member import Member from .snowflake import Snowflake +from .threads import Thread, ThreadMember +from .user import User class _MessageEventOptional(TypedDict, total=False): @@ -41,55 +43,47 @@ class MessageDeleteEvent(_MessageEventOptional): class BulkMessageDeleteEvent(_MessageEventOptional): - ids: List[Snowflake] + ids: list[Snowflake] channel_id: Snowflake -class _ReactionActionEventOptional(TypedDict, total=False): - guild_id: Snowflake - member: Member - - class MessageUpdateEvent(_MessageEventOptional): id: Snowflake channel_id: Snowflake -class ReactionActionEvent(_ReactionActionEventOptional): +class _ReactionEventOptional(TypedDict, total=False): + guild_id: Snowflake + + +class ReactionActionEvent(_ReactionEventOptional): + member: NotRequired[Member] user_id: Snowflake channel_id: Snowflake message_id: Snowflake emoji: PartialEmoji -class _ReactionClearEventOptional(TypedDict, total=False): - guild_id: Snowflake - - -class ReactionClearEvent(_ReactionClearEventOptional): +class ReactionClearEvent(_ReactionEventOptional): channel_id: Snowflake message_id: Snowflake -class _ReactionClearEmojiEventOptional(TypedDict, total=False): - guild_id: Snowflake - - -class ReactionClearEmojiEvent(_ReactionClearEmojiEventOptional): +class ReactionClearEmojiEvent(_ReactionEventOptional): channel_id: int message_id: int emoji: PartialEmoji -class _IntegrationDeleteEventOptional(TypedDict, total=False): - application_id: Snowflake - - -class IntegrationDeleteEvent(_IntegrationDeleteEventOptional): +class IntegrationDeleteEvent(TypedDict): + application_id: NotRequired[Snowflake] id: Snowflake guild_id: Snowflake +ThreadUpdateEvent = Thread + + class ThreadDeleteEvent(TypedDict, total=False): thread_id: Snowflake thread_type: int @@ -97,12 +91,9 @@ class ThreadDeleteEvent(TypedDict, total=False): parent_id: Snowflake -class _TypingEventOptional(TypedDict, total=False): - guild_id: Snowflake - member: Member - - -class TypingEvent(_TypingEventOptional): +class TypingEvent(TypedDict): + guild_id: NotRequired[Snowflake] + member: NotRequired[Member] channel_id: Snowflake user_id: Snowflake timestamp: int @@ -114,18 +105,28 @@ class ScheduledEventSubscription(TypedDict, total=False): guild_id: Snowflake -class _AutoModActionExecutionEventOptional(TypedDict, total=False): - channel_id: Snowflake - message_id: Snowflake - alert_system_message_id: Snowflake - matched_keyword: str - matched_content: str - - -class AutoModActionExecutionEvent(_AutoModActionExecutionEventOptional): +class AutoModActionExecutionEvent(TypedDict): + channel_id: NotRequired[Snowflake] + message_id: NotRequired[Snowflake] + alert_system_message_id: NotRequired[Snowflake] + matched_keyword: NotRequired[str] + matched_content: NotRequired[str] guild_id: Snowflake action: AutoModAction rule_id: Snowflake rule_trigger_type: AutoModTriggerType user_id: Snowflake content: str + + +class MemberRemoveEvent(TypedDict): + guild_id: Snowflake + user: User + + +class ThreadMembersUpdateEvent(TypedDict): + id: Snowflake + guild_id: Snowflake + member_count: int + added_members: NotRequired[list[ThreadMember]] + removed_member_ids: NotRequired[list[Snowflake]] diff --git a/discord/types/role.py b/discord/types/role.py index 2b3199e5..8996e7e2 100644 --- a/discord/types/role.py +++ b/discord/types/role.py @@ -25,16 +25,12 @@ from __future__ import annotations -from typing import TypedDict - +from .._typed_dict import NotRequired, TypedDict from .snowflake import Snowflake -class _RoleOptional(TypedDict, total=False): - tags: RoleTags - - -class Role(_RoleOptional): +class Role(TypedDict): + tags: NotRequired[RoleTags] id: Snowflake name: str color: int diff --git a/discord/types/sticker.py b/discord/types/sticker.py index 05c85a1e..129b14cc 100644 --- a/discord/types/sticker.py +++ b/discord/types/sticker.py @@ -25,8 +25,9 @@ from __future__ import annotations -from typing import Literal, TypedDict, Union +from typing import Literal, Union +from .._typed_dict import NotRequired, TypedDict from .snowflake import Snowflake from .user import User @@ -53,11 +54,8 @@ class StandardSticker(BaseSticker): pack_id: Snowflake -class _GuildStickerOptional(TypedDict, total=False): - user: User - - -class GuildSticker(BaseSticker, _GuildStickerOptional): +class GuildSticker(BaseSticker): + user: NotRequired[User] type: Literal[2] available: bool guild_id: Snowflake @@ -76,11 +74,8 @@ class StickerPack(TypedDict): banner_asset_id: Snowflake -class _CreateGuildStickerOptional(TypedDict, total=False): - description: str - - -class CreateGuildSticker(_CreateGuildStickerOptional): +class CreateGuildSticker(TypedDict): + description: NotRequired[str] name: str tags: str diff --git a/discord/types/threads.py b/discord/types/threads.py index ec84ba87..aab6d957 100644 --- a/discord/types/threads.py +++ b/discord/types/threads.py @@ -25,8 +25,10 @@ from __future__ import annotations -from typing import Literal, TypedDict +from typing import Literal +from .._typed_dict import NotRequired, TypedDict +from ..flags import ChannelFlags from .snowflake import Snowflake ThreadType = Literal[10, 11, 12] @@ -40,25 +42,19 @@ class ThreadMember(TypedDict): flags: int -class _ThreadMetadataOptional(TypedDict, total=False): - invitable: bool - create_timestamp: str - - -class ThreadMetadata(_ThreadMetadataOptional): +class ThreadMetadata(TypedDict): + invitable: NotRequired[bool] + create_timestamp: NotRequired[str] archived: bool auto_archive_duration: ThreadArchiveDuration archive_timestamp: str locked: bool -class _ThreadOptional(TypedDict, total=False): - member: ThreadMember - last_message_id: Snowflake | None - last_pin_timestamp: Snowflake | None - - -class Thread(_ThreadOptional): +class Thread(TypedDict): + member: NotRequired[ThreadMember] + last_message_id: NotRequired[Snowflake | None] + last_pin_timestamp: NotRequired[Snowflake | None] id: Snowflake guild_id: Snowflake parent_id: Snowflake @@ -69,6 +65,8 @@ class Thread(_ThreadOptional): message_count: int rate_limit_per_user: int thread_metadata: ThreadMetadata + flags: ChannelFlags + total_message_sent: int class ThreadPaginationPayload(TypedDict): diff --git a/discord/types/user.py b/discord/types/user.py index a344cac9..233eb4e4 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -22,8 +22,9 @@ 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 Literal, Optional, TypedDict +from typing import Literal, TypedDict from .snowflake import Snowflake @@ -32,7 +33,7 @@ class PartialUser(TypedDict): id: Snowflake username: str discriminator: str - avatar: Optional[str] + avatar: str | None PremiumType = Literal[0, 1, 2] @@ -44,7 +45,7 @@ class User(PartialUser, total=False): mfa_enabled: bool local: str verified: bool - email: Optional[str] + email: str | None flags: int premium_type: PremiumType public_flags: int diff --git a/discord/types/voice.py b/discord/types/voice.py index e4dbe8c4..353bf102 100644 --- a/discord/types/voice.py +++ b/discord/types/voice.py @@ -22,9 +22,11 @@ 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 List, Literal, Optional, TypedDict +from typing import Literal +from .._typed_dict import NotRequired, TypedDict from .member import MemberWithUser from .snowflake import Snowflake @@ -33,12 +35,9 @@ ] -class _PartialVoiceStateOptional(TypedDict, total=False): - member: MemberWithUser - self_stream: bool - - -class _VoiceState(_PartialVoiceStateOptional): +class _VoiceState(TypedDict): + member: NotRequired[MemberWithUser] + self_stream: NotRequired[bool] user_id: Snowflake session_id: str deaf: bool @@ -54,7 +53,7 @@ class GuildVoiceState(_VoiceState): class VoiceState(_VoiceState, total=False): - channel_id: Optional[Snowflake] + channel_id: Snowflake | None guild_id: Snowflake @@ -70,7 +69,7 @@ class VoiceRegion(TypedDict): class VoiceServerUpdate(TypedDict): token: str guild_id: Snowflake - endpoint: Optional[str] + endpoint: str | None class VoiceIdentify(TypedDict): @@ -84,5 +83,5 @@ class VoiceReady(TypedDict): ssrc: int ip: str port: int - modes: List[SupportedModes] + modes: list[SupportedModes] heartbeat_interval: int diff --git a/discord/types/webhook.py b/discord/types/webhook.py index cb96791d..b5db096e 100644 --- a/discord/types/webhook.py +++ b/discord/types/webhook.py @@ -25,8 +25,9 @@ from __future__ import annotations -from typing import Literal, TypedDict +from typing import Literal +from .._typed_dict import NotRequired, TypedDict from .channel import PartialChannel from .snowflake import Snowflake from .user import User @@ -38,36 +39,26 @@ class SourceGuild(TypedDict): icon: str -class _WebhookOptional(TypedDict, total=False): - guild_id: Snowflake - user: User - token: str - - WebhookType = Literal[1, 2, 3] -class _FollowerWebhookOptional(TypedDict, total=False): - source_channel: PartialChannel - source_guild: SourceGuild - - -class FollowerWebhook(_FollowerWebhookOptional): +class FollowerWebhook(TypedDict): + source_channel: NotRequired[PartialChannel] + source_guild: NotRequired[SourceGuild] channel_id: Snowflake webhook_id: Snowflake -class PartialWebhook(_WebhookOptional): +class PartialWebhook(TypedDict): + guild_id: NotRequired[Snowflake] + user: NotRequired[User] + token: NotRequired[str] id: Snowflake type: WebhookType -class _FullWebhook(TypedDict, total=False): - name: str | None - avatar: str | None - channel_id: Snowflake - application_id: Snowflake | None - - -class Webhook(PartialWebhook, _FullWebhook): - pass +class Webhook(PartialWebhook): + name: NotRequired[str | None] + avatar: NotRequired[str | None] + channel_id: NotRequired[Snowflake] + application_id: NotRequired[Snowflake | None] diff --git a/discord/types/widget.py b/discord/types/widget.py index c61103a4..daeef2db 100644 --- a/discord/types/widget.py +++ b/discord/types/widget.py @@ -22,9 +22,9 @@ 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 List, Optional, TypedDict - +from .._typed_dict import NotRequired, TypedDict from .activity import Activity from .snowflake import Snowflake from .user import User @@ -48,13 +48,10 @@ class WidgetMember(User, total=False): suppress: bool -class _WidgetOptional(TypedDict, total=False): - channels: List[WidgetChannel] - members: List[WidgetMember] - presence_count: int - - -class Widget(_WidgetOptional): +class Widget(TypedDict): + channels: NotRequired[list[WidgetChannel]] + members: NotRequired[list[WidgetMember]] + presence_count: NotRequired[int] id: Snowflake name: str instant_invite: str @@ -62,4 +59,4 @@ class Widget(_WidgetOptional): class WidgetSettings(TypedDict): enabled: bool - channel_id: Optional[Snowflake] + channel_id: Snowflake | None diff --git a/discord/ui/button.py b/discord/ui/button.py index eb6d54e9..c607de45 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -64,7 +64,7 @@ class Button(Item[V]): disabled: :class:`bool` Whether the button is disabled or not. label: Optional[:class:`str`] - The label of the button, if any. + The label of the button, if any. Maximum of 80 chars. emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]] The emoji of the button, if available. row: Optional[:class:`int`] @@ -122,7 +122,8 @@ def __init__( emoji = emoji._to_partial() else: raise TypeError( - f"expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}" + "expected emoji to be str, Emoji, or PartialEmoji not" + f" {emoji.__class__}" ) self._underlying = ButtonComponent._raw_construct( @@ -138,7 +139,7 @@ def __init__( @property def style(self) -> ButtonStyle: - """:class:`discord.ButtonStyle`: The style of the button.""" + """The style of the button.""" return self._underlying.style @style.setter @@ -147,7 +148,7 @@ def style(self, value: ButtonStyle): @property def custom_id(self) -> str | None: - """Optional[:class:`str`]: The ID of the button that gets received during an interaction. + """The ID of the button that gets received during an interaction. If this button is for a URL, it does not have a custom ID. """ @@ -163,7 +164,7 @@ def custom_id(self, value: str | None): @property def url(self) -> str | None: - """Optional[:class:`str`]: The URL this button sends you to.""" + """The URL this button sends you to.""" return self._underlying.url @url.setter @@ -174,7 +175,7 @@ def url(self, value: str | None): @property def disabled(self) -> bool: - """:class:`bool`: Whether the button is disabled or not.""" + """Whether the button is disabled or not.""" return self._underlying.disabled @disabled.setter @@ -183,7 +184,7 @@ def disabled(self, value: bool): @property def label(self) -> str | None: - """Optional[:class:`str`]: The label of the button, if available.""" + """The label of the button, if available.""" return self._underlying.label @label.setter @@ -194,7 +195,7 @@ def label(self, value: str | None): @property def emoji(self) -> PartialEmoji | None: - """Optional[:class:`.PartialEmoji`]: The emoji of the button, if available.""" + """The emoji of the button, if available.""" return self._underlying.emoji @emoji.setter @@ -207,7 +208,8 @@ def emoji(self, value: str | Emoji | PartialEmoji | None): # type: ignore self._underlying.emoji = value._to_partial() else: raise TypeError( - f"expected str, Emoji, or PartialEmoji, received {value.__class__} instead" + "expected str, Emoji, or PartialEmoji, received" + f" {value.__class__} instead" ) @classmethod diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index ce552fd2..115f9708 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -99,7 +99,7 @@ def type(self) -> ComponentType: @property def style(self) -> InputTextStyle: - """:class:`~discord.InputTextStyle`: The style of the input text field.""" + """The style of the input text field.""" return self._underlying.style @style.setter @@ -112,7 +112,7 @@ def style(self, value: InputTextStyle): @property def custom_id(self) -> str: - """:class:`str`: The ID of the input text field that gets received during an interaction.""" + """The ID of the input text field that gets received during an interaction.""" return self._underlying.custom_id @custom_id.setter @@ -125,7 +125,7 @@ def custom_id(self, value: str): @property def label(self) -> str: - """:class:`str`: The label of the input text field.""" + """The label of the input text field.""" return self._underlying.label @label.setter @@ -138,7 +138,7 @@ def label(self, value: str): @property def placeholder(self) -> str | None: - """Optional[:class:`str`]: The placeholder text that is shown before anything is entered, if any.""" + """The placeholder text that is shown before anything is entered, if any.""" return self._underlying.placeholder @placeholder.setter @@ -151,7 +151,7 @@ def placeholder(self, value: str | None): @property def min_length(self) -> int | None: - """Optional[:class:`int`]: The minimum number of characters that must be entered. Defaults to `0`.""" + """The minimum number of characters that must be entered. Defaults to `0`.""" return self._underlying.min_length @min_length.setter @@ -164,7 +164,7 @@ def min_length(self, value: int | None): @property def max_length(self) -> int | None: - """Optional[:class:`int`]: The maximum number of characters that can be entered.""" + """The maximum number of characters that can be entered.""" return self._underlying.max_length @max_length.setter @@ -177,7 +177,7 @@ def max_length(self, value: int | None): @property def required(self) -> bool | None: - """Optional[:class:`bool`]: Whether the input text field is required or not. Defaults to `True`.""" + """Whether the input text field is required or not. Defaults to `True`.""" return self._underlying.required @required.setter @@ -188,7 +188,7 @@ def required(self, value: bool | None): @property def value(self) -> str | None: - """Optional[:class:`str`]: The value entered in the text field.""" + """The value entered in the text field.""" if self._input_value is not False: # only False on init, otherwise the value was either set or cleared return self._input_value # type: ignore diff --git a/discord/ui/item.py b/discord/ui/item.py index ea70fea2..c822983b 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -114,7 +114,7 @@ def width(self) -> int: @property def view(self) -> V | None: - """Optional[:class:`View`]: The underlying view for this item.""" + """The underlying view for this item.""" return self._view async def callback(self, interaction: Interaction): diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 9d985c4a..966e6abe 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -113,7 +113,7 @@ def _dispatch_timeout(self): @property def title(self) -> str: - """:class:`str`: The title of the modal dialog.""" + """The title of the modal dialog.""" return self._title @title.setter @@ -126,7 +126,7 @@ def title(self, value: str): @property def children(self) -> list[InputText]: - """List[:class:`InputText`]: The child components associated with the modal dialog.""" + """The child components associated with the modal dialog.""" return self._children @children.setter @@ -134,14 +134,15 @@ def children(self, value: list[InputText]): for item in value: if not isinstance(item, InputText): raise TypeError( - f"all Modal children must be InputText, not {item.__class__.__name__}" + "all Modal children must be InputText, not" + f" {item.__class__.__name__}" ) self._weights = _ModalWeights(self._children) self._children = value @property def custom_id(self) -> str: - """:class:`str`: The ID of the modal dialog that gets received during an interaction.""" + """The ID of the modal dialog that gets received during an interaction.""" return self._custom_id @custom_id.setter diff --git a/discord/ui/select.py b/discord/ui/select.py index c99d5f80..5b373bd1 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -29,20 +29,32 @@ import os from typing import TYPE_CHECKING, Callable, TypeVar +from ..channel import _threaded_guild_channel_factory from ..components import SelectMenu, SelectOption from ..emoji import Emoji -from ..enums import ComponentType +from ..enums import ChannelType, ComponentType +from ..errors import InvalidArgument from ..interactions import Interaction +from ..member import Member from ..partial_emoji import PartialEmoji +from ..role import Role +from ..threads import Thread +from ..user import User from ..utils import MISSING from .item import Item, ItemCallbackType __all__ = ( "Select", "select", + "string_select", + "user_select", + "role_select", + "mentionable_select", + "channel_select", ) if TYPE_CHECKING: + from ..abc import GuildChannel from ..types.components import SelectMenu as SelectMenuPayload from ..types.interactions import ComponentInteractionData from .view import View @@ -60,8 +72,19 @@ class Select(Item[V]): .. versionadded:: 2.0 + .. versionchanged:: 2.3 + + Added support for :attr:`discord.ComponentType.string_select`, :attr:`discord.ComponentType.user_select`, + :attr:`discord.ComponentType.role_select`, :attr:`discord.ComponentType.mentionable_select`, + and :attr:`discord.ComponentType.channel_select`. + Parameters ---------- + select_type: :class:`discord.ComponentType` + The type of select to create. Must be one of + :attr:`discord.ComponentType.string_select`, :attr:`discord.ComponentType.user_select`, + :attr:`discord.ComponentType.role_select`, :attr:`discord.ComponentType.mentionable_select`, + or :attr:`discord.ComponentType.channel_select`. custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. @@ -75,6 +98,10 @@ class Select(Item[V]): Defaults to 1 and must be between 1 and 25. options: List[:class:`discord.SelectOption`] A list of options that can be selected in this menu. + Only valid for selects of type :attr:`discord.ComponentType.string_select`. + channel_types: List[:class:`discord.ChannelType`] + A list of channel types that can be selected in this menu. + Only valid for selects of type :attr:`discord.ComponentType.channel_select`. disabled: :class:`bool` Whether the select is disabled or not. row: Optional[:class:`int`] @@ -86,26 +113,37 @@ class Select(Item[V]): """ __item_repr_attributes__: tuple[str, ...] = ( + "type", "placeholder", "min_values", "max_values", "options", + "channel_types", "disabled", ) def __init__( self, + select_type: ComponentType = ComponentType.string_select, *, custom_id: str | None = None, placeholder: str | None = None, min_values: int = 1, max_values: int = 1, - options: list[SelectOption] = MISSING, + options: list[SelectOption] = None, + channel_types: list[ChannelType] = None, disabled: bool = False, row: int | None = None, ) -> None: + if options and select_type is not ComponentType.string_select: + raise InvalidArgument("options parameter is only valid for string selects") + if channel_types and select_type is not ComponentType.channel_select: + raise InvalidArgument( + "channel_types parameter is only valid for channel selects" + ) super().__init__() self._selected_values: list[str] = [] + self._interaction: Interaction | None = None if min_values < 0 or min_values > 25: raise ValueError("min_values must be between 0 and 25") if max_values < 1 or max_values > 25: @@ -119,21 +157,21 @@ def __init__( self._provided_custom_id = custom_id is not None custom_id = os.urandom(16).hex() if custom_id is None else custom_id - options = [] if options is MISSING else options - self._underlying = SelectMenu._raw_construct( + self._underlying: SelectMenu = SelectMenu._raw_construct( custom_id=custom_id, - type=ComponentType.select, + type=select_type, placeholder=placeholder, min_values=min_values, max_values=max_values, - options=options, disabled=disabled, + options=options or [], + channel_types=channel_types or [], ) self.row = row @property def custom_id(self) -> str: - """:class:`str`: The ID of the select menu that gets received during an interaction.""" + """The ID of the select menu that gets received during an interaction.""" return self._underlying.custom_id @custom_id.setter @@ -146,7 +184,7 @@ def custom_id(self, value: str): @property def placeholder(self) -> str | None: - """Optional[:class:`str`]: The placeholder text that is shown if nothing is selected, if any.""" + """The placeholder text that is shown if nothing is selected, if any.""" return self._underlying.placeholder @placeholder.setter @@ -160,7 +198,7 @@ def placeholder(self, value: str | None): @property def min_values(self) -> int: - """:class:`int`: The minimum number of items that must be chosen for this select menu.""" + """The minimum number of items that must be chosen for this select menu.""" return self._underlying.min_values @min_values.setter @@ -171,7 +209,7 @@ def min_values(self, value: int): @property def max_values(self) -> int: - """:class:`int`: The maximum number of items that must be chosen for this select menu.""" + """The maximum number of items that must be chosen for this select menu.""" return self._underlying.max_values @max_values.setter @@ -180,13 +218,35 @@ def max_values(self, value: int): raise ValueError("max_values must be between 1 and 25") self._underlying.max_values = int(value) + @property + def disabled(self) -> bool: + """Whether the select is disabled or not.""" + return self._underlying.disabled + + @disabled.setter + def disabled(self, value: bool): + self._underlying.disabled = bool(value) + + @property + def channel_types(self) -> list[ChannelType]: + """A list of channel types that can be selected in this menu.""" + return self._underlying.channel_types + + @channel_types.setter + def channel_types(self, value: list[ChannelType]): + if self._underlying.type is not ComponentType.channel_select: + raise InvalidArgument("channel_types can only be set on channel selects") + self._underlying.channel_types = value + @property def options(self) -> list[SelectOption]: - """List[:class:`discord.SelectOption`]: A list of options that can be selected in this menu.""" + """A list of options that can be selected in this menu.""" return self._underlying.options @options.setter def options(self, value: list[SelectOption]): + if self._underlying.type is not ComponentType.string_select: + raise InvalidArgument("options can only be set on string selects") if not isinstance(value, list): raise TypeError("options must be a list of SelectOption") if not all(isinstance(obj, SelectOption) for obj in value): @@ -230,6 +290,8 @@ def add_option( ValueError The number of options exceeds 25. """ + if self._underlying.type is not ComponentType.string_select: + raise Exception("options can only be set on string selects") option = SelectOption( label=label, @@ -254,6 +316,8 @@ def append_option(self, option: SelectOption): ValueError The number of options exceeds 25. """ + if self._underlying.type is not ComponentType.string_select: + raise Exception("options can only be set on string selects") if len(self._underlying.options) > 25: raise ValueError("maximum number of options already provided") @@ -261,18 +325,77 @@ def append_option(self, option: SelectOption): self._underlying.options.append(option) @property - def disabled(self) -> bool: - """:class:`bool`: Whether the select is disabled or not.""" - return self._underlying.disabled - - @disabled.setter - def disabled(self, value: bool): - self._underlying.disabled = bool(value) - - @property - def values(self) -> list[str]: - """List[:class:`str`]: A list of values that have been selected by the user.""" - return self._selected_values + def values( + self, + ) -> ( + list[str] + | list[Member | User] + | list[Role] + | list[Member | User | Role] + | list[GuildChannel | Thread] + ): + """Union[List[:class:`str`], List[Union[:class:`discord.Member`, :class:`discord.User`]], List[:class:`discord.Role`]], + List[Union[:class:`discord.Member`, :class:`discord.User`, :class:`discord.Role`]], List[:class:`discord.abc.GuildChannel`]]: + A list of values that have been selected by the user. + """ + select_type = self._underlying.type + if select_type is ComponentType.string_select: + return self._selected_values + resolved = [] + selected_values = list(self._selected_values) + state = self._interaction._state + guild = self._interaction.guild + resolved_data = self._interaction.data.get("resolved", {}) + if select_type is ComponentType.channel_select: + for channel_id, _data in resolved_data.get("channels", {}).items(): + if channel_id not in selected_values: + continue + if ( + int(channel_id) in guild._channels + or int(channel_id) in guild._threads + ): + result = guild.get_channel_or_thread(int(channel_id)) + _data["_invoke_flag"] = True + ( + result._update(_data) + if isinstance(result, Thread) + else result._update(guild, _data) + ) + else: + # NOTE: + # This is a fallback in case the channel/thread is not found in the + # guild's channels/threads. For channels, if this fallback occurs, at the very minimum, + # permissions will be incorrect due to a lack of permission_overwrite data. + # For threads, if this fallback occurs, info like thread owner id, message count, + # flags, and more will be missing due to a lack of data sent by Discord. + obj_type = _threaded_guild_channel_factory(_data["type"])[0] + result = obj_type(state=state, data=_data, guild=guild) + resolved.append(result) + elif select_type in ( + ComponentType.user_select, + ComponentType.mentionable_select, + ): + cache_flag = state.member_cache_flags.interaction + resolved_user_data = resolved_data.get("users", {}) + resolved_member_data = resolved_data.get("members", {}) + for _id in selected_values: + if (_data := resolved_user_data.get(_id)) is not None: + if (_member_data := resolved_member_data.get(_id)) is not None: + member = dict(_member_data) + member["user"] = _data + _data = member + result = guild._get_and_update_member( + _data, int(_id), cache_flag + ) + else: + result = User(state=state, data=_data) + resolved.append(result) + if select_type in (ComponentType.role_select, ComponentType.mentionable_select): + for role_id, _data in resolved_data.get("roles", {}).items(): + if role_id not in selected_values: + continue + resolved.append(Role(guild=guild, state=state, data=_data)) + return resolved @property def width(self) -> int: @@ -287,15 +410,18 @@ def refresh_component(self, component: SelectMenu) -> None: def refresh_state(self, interaction: Interaction) -> None: data: ComponentInteractionData = interaction.data # type: ignore self._selected_values = data.get("values", []) + self._interaction = interaction @classmethod def from_component(cls: type[S], component: SelectMenu) -> S: return cls( + select_type=component.type, custom_id=component.custom_id, placeholder=component.placeholder, min_values=component.min_values, max_values=component.max_values, options=component.options, + channel_types=component.channel_types, disabled=component.disabled, row=None, ) @@ -308,13 +434,24 @@ def is_dispatchable(self) -> bool: return True +_select_types = ( + ComponentType.string_select, + ComponentType.user_select, + ComponentType.role_select, + ComponentType.mentionable_select, + ComponentType.channel_select, +) + + def select( + select_type: ComponentType = ComponentType.string_select, *, placeholder: str | None = None, custom_id: str | None = None, min_values: int = 1, max_values: int = 1, options: list[SelectOption] = MISSING, + channel_types: list[ChannelType] = MISSING, disabled: bool = False, row: int | None = None, ) -> Callable[[ItemCallbackType], ItemCallbackType]: @@ -327,8 +464,17 @@ def select( In order to get the selected items that the user has chosen within the callback use :attr:`Select.values`. + .. versionchanged:: 2.3 + + Creating select menus of different types is now supported. + Parameters ---------- + select_type: :class:`discord.ComponentType` + The type of select to create. Must be one of + :attr:`discord.ComponentType.string_select`, :attr:`discord.ComponentType.user_select`, + :attr:`discord.ComponentType.role_select`, :attr:`discord.ComponentType.mentionable_select`, + or :attr:`discord.ComponentType.channel_select`. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. custom_id: :class:`str` @@ -348,24 +494,173 @@ def select( Defaults to 1 and must be between 1 and 25. options: List[:class:`discord.SelectOption`] A list of options that can be selected in this menu. + Only valid for the :attr:`discord.ComponentType.string_select` type. + channel_types: List[:class:`discord.ChannelType`] + The channel types that should be selectable. + Only valid for the :attr:`discord.ComponentType.channel_select` type. + Defaults to all channel types. disabled: :class:`bool` Whether the select is disabled or not. Defaults to ``False``. """ + if select_type not in _select_types: + raise ValueError( + "select_type must be one of " + ", ".join([i.name for i in _select_types]) + ) + + if options is not MISSING and select_type not in ( + ComponentType.select, + ComponentType.string_select, + ): + raise TypeError("options may only be specified for string selects") + + if channel_types is not MISSING and select_type is not ComponentType.channel_select: + raise TypeError("channel_types may only be specified for channel selects") def decorator(func: ItemCallbackType) -> ItemCallbackType: if not inspect.iscoroutinefunction(func): raise TypeError("select function must be a coroutine function") - func.__discord_ui_model_type__ = Select - func.__discord_ui_model_kwargs__ = { + model_kwargs = { + "select_type": select_type, "placeholder": placeholder, "custom_id": custom_id, "row": row, "min_values": min_values, "max_values": max_values, - "options": options, "disabled": disabled, } + if options: + model_kwargs["options"] = options + if channel_types: + model_kwargs["channel_types"] = channel_types + + func.__discord_ui_model_type__ = Select + func.__discord_ui_model_kwargs__ = model_kwargs + return func return decorator + + +def string_select( + *, + placeholder: str | None = None, + custom_id: str | None = None, + min_values: int = 1, + max_values: int = 1, + options: list[SelectOption] = MISSING, + disabled: bool = False, + row: int | None = None, +) -> Callable[[ItemCallbackType], ItemCallbackType]: + """A shortcut for :meth:`discord.ui.select` with select type :attr:`discord.ComponentType.string_select`. + + .. versionadded:: 2.3 + """ + return select( + ComponentType.string_select, + placeholder=placeholder, + custom_id=custom_id, + min_values=min_values, + max_values=max_values, + options=options, + disabled=disabled, + row=row, + ) + + +def user_select( + *, + placeholder: str | None = None, + custom_id: str | None = None, + min_values: int = 1, + max_values: int = 1, + disabled: bool = False, + row: int | None = None, +) -> Callable[[ItemCallbackType], ItemCallbackType]: + """A shortcut for :meth:`discord.ui.select` with select type :attr:`discord.ComponentType.user_select`. + + .. versionadded:: 2.3 + """ + return select( + ComponentType.user_select, + placeholder=placeholder, + custom_id=custom_id, + min_values=min_values, + max_values=max_values, + disabled=disabled, + row=row, + ) + + +def role_select( + *, + placeholder: str | None = None, + custom_id: str | None = None, + min_values: int = 1, + max_values: int = 1, + disabled: bool = False, + row: int | None = None, +) -> Callable[[ItemCallbackType], ItemCallbackType]: + """A shortcut for :meth:`discord.ui.select` with select type :attr:`discord.ComponentType.role_select`. + + .. versionadded:: 2.3 + """ + return select( + ComponentType.role_select, + placeholder=placeholder, + custom_id=custom_id, + min_values=min_values, + max_values=max_values, + disabled=disabled, + row=row, + ) + + +def mentionable_select( + *, + placeholder: str | None = None, + custom_id: str | None = None, + min_values: int = 1, + max_values: int = 1, + disabled: bool = False, + row: int | None = None, +) -> Callable[[ItemCallbackType], ItemCallbackType]: + """A shortcut for :meth:`discord.ui.select` with select type :attr:`discord.ComponentType.mentionable_select`. + + .. versionadded:: 2.3 + """ + return select( + ComponentType.mentionable_select, + placeholder=placeholder, + custom_id=custom_id, + min_values=min_values, + max_values=max_values, + disabled=disabled, + row=row, + ) + + +def channel_select( + *, + placeholder: str | None = None, + custom_id: str | None = None, + min_values: int = 1, + max_values: int = 1, + disabled: bool = False, + channel_types: list[ChannelType] = MISSING, + row: int | None = None, +) -> Callable[[ItemCallbackType], ItemCallbackType]: + """A shortcut for :meth:`discord.ui.select` with select type :attr:`discord.ComponentType.channel_select`. + + .. versionadded:: 2.3 + """ + return select( + ComponentType.channel_select, + placeholder=placeholder, + custom_id=custom_id, + min_values=min_values, + max_values=max_values, + disabled=disabled, + channel_types=channel_types, + row=row, + ) diff --git a/discord/ui/view.py b/discord/ui/view.py index 20e2b291..8172e726 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -483,15 +483,15 @@ def stop(self) -> None: self.__cancel_callback = None def is_finished(self) -> bool: - """:class:`bool`: Whether the view has finished interacting.""" + """Whether the view has finished interacting.""" return self.__stopped.done() def is_dispatching(self) -> bool: - """:class:`bool`: Whether the view has been added for dispatching purposes.""" + """Whether the view has been added for dispatching purposes.""" return self.__cancel_callback is not None def is_persistent(self) -> bool: - """:class:`bool`: Whether the view is set up as persistent. + """Whether the view is set up as persistent. A persistent view has all their components with a set ``custom_id`` and a :attr:`timeout` set to ``None``. @@ -568,7 +568,7 @@ def persistent_views(self) -> Sequence[View]: def __verify_integrity(self): to_remove: list[tuple[int, int | None, str]] = [] - for (k, (view, _)) in self._views.items(): + for k, (view, _) in self._views.items(): if view.is_finished(): to_remove.append(k) diff --git a/discord/user.py b/discord/user.py index 3541c338..6a322f98 100644 --- a/discord/user.py +++ b/discord/user.py @@ -91,7 +91,8 @@ def __init__(self, *, state: ConnectionState, data: UserPayload) -> None: def __repr__(self) -> str: return ( - f"" ) @@ -145,7 +146,7 @@ def _to_minimal_user_json(self) -> dict[str, Any]: @property def jump_url(self) -> str: - """:class:`str`: Returns a URL that allows the client to jump to the user. + """Returns a URL that allows the client to jump to the user. .. versionadded:: 2.0 """ @@ -153,12 +154,12 @@ def jump_url(self) -> str: @property def public_flags(self) -> PublicUserFlags: - """:class:`PublicUserFlags`: The publicly available flags the user has.""" + """The publicly available flags the user has.""" return PublicUserFlags._from_value(self._public_flags) @property def avatar(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns an :class:`Asset` for the avatar the user has. + """Returns an :class:`Asset` for the avatar the user has. If the user does not have a traditional avatar, ``None`` is returned. If you want the avatar that a user has displayed, consider :attr:`display_avatar`. @@ -169,7 +170,7 @@ def avatar(self) -> Asset | None: @property def default_avatar(self) -> Asset: - """:class:`Asset`: Returns the default avatar for a given user. + """Returns the default avatar for a given user. This is calculated by the user's discriminator. """ return Asset._from_default_avatar( @@ -178,7 +179,7 @@ def default_avatar(self) -> Asset: @property def display_avatar(self) -> Asset: - """:class:`Asset`: Returns the user's display avatar. + """Returns the user's display avatar. For regular users this is just their default avatar or uploaded avatar. @@ -188,7 +189,7 @@ def display_avatar(self) -> Asset: @property def banner(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the user's banner asset, if available. + """Returns the user's banner asset, if available. .. versionadded:: 2.0 @@ -201,7 +202,7 @@ def banner(self) -> Asset | None: @property def accent_colour(self) -> Colour | None: - """Optional[:class:`Colour`]: Returns the user's accent colour, if applicable. + """Returns the user's accent colour, if applicable. There is an alias for this named :attr:`accent_color`. @@ -217,7 +218,7 @@ def accent_colour(self) -> Colour | None: @property def accent_color(self) -> Colour | None: - """Optional[:class:`Colour`]: Returns the user's accent color, if applicable. + """Returns the user's accent color, if applicable. There is an alias for this named :attr:`accent_colour`. @@ -231,7 +232,7 @@ def accent_color(self) -> Colour | None: @property def colour(self) -> Colour: - """:class:`Colour`: A property that returns a colour denoting the rendered colour + """A property that returns a colour denoting the rendered colour for the user. This always returns :meth:`Colour.default`. There is an alias for this named :attr:`color`. @@ -240,7 +241,7 @@ def colour(self) -> Colour: @property def color(self) -> Colour: - """:class:`Colour`: A property that returns a color denoting the rendered color + """A property that returns a color denoting the rendered color for the user. This always returns :meth:`Colour.default`. There is an alias for this named :attr:`colour`. @@ -249,12 +250,12 @@ def color(self) -> Colour: @property def mention(self) -> str: - """:class:`str`: Returns a string that allows you to mention the given user.""" + """Returns a string that allows you to mention the given user.""" return f"<@{self.id}>" @property def created_at(self) -> datetime: - """:class:`datetime.datetime`: Returns the user's creation time in UTC. + """Returns the user's creation time in UTC. This is when the user's Discord account was created. """ @@ -262,7 +263,7 @@ def created_at(self) -> datetime: @property def display_name(self) -> str: - """:class:`str`: Returns the user's display name. + """Returns the user's display name. For regular users this is just their username, but if they have a guild specific nickname then that @@ -347,7 +348,8 @@ def __init__(self, *, state: ConnectionState, data: UserPayload) -> None: def __repr__(self) -> str: return ( - f"" ) @@ -451,7 +453,10 @@ def __init__(self, *, state: ConnectionState, data: UserPayload) -> None: self._stored: bool = False def __repr__(self) -> str: - return f"" + return ( + "" + ) def __del__(self) -> None: try: @@ -472,7 +477,7 @@ async def _get_channel(self) -> DMChannel: @property def dm_channel(self) -> DMChannel | None: - """Optional[:class:`DMChannel`]: Returns the channel associated with this user if it exists. + """Returns the channel associated with this user if it exists. If this returns ``None``, you can create a DM channel by calling the :meth:`create_dm` coroutine function. @@ -481,7 +486,7 @@ def dm_channel(self) -> DMChannel | None: @property def mutual_guilds(self) -> list[Guild]: - """List[:class:`Guild`]: The guilds that the user shares with the client. + """The guilds that the user shares with the client. .. note:: diff --git a/discord/utils.py b/discord/utils.py index cf59e979..20add932 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -38,6 +38,7 @@ import warnings from base64 import b64encode from bisect import bisect_left +from dataclasses import field from inspect import isawaitable as _isawaitable from inspect import signature as _signature from operator import attrgetter @@ -73,13 +74,19 @@ __all__ = ( + "parse_time", + "warn_deprecated", + "deprecated", "oauth_url", "snowflake_time", "time_snowflake", "find", "get", + "get_or_fetch", "sleep_until", "utcnow", + "resolve_invite", + "resolve_template", "remove_markdown", "escape_markdown", "escape_mentions", @@ -88,8 +95,8 @@ "raw_role_mentions", "as_chunks", "format_dt", - "basic_autocomplete", "generate_snowflake", + "basic_autocomplete", "filter_params", ) @@ -108,6 +115,11 @@ def __repr__(self) -> str: MISSING: Any = _MissingSentinel() +# As of 3.11, directly setting a dataclass field to MISSING causes a ValueError. Using +# field(default=MISSING) produces the same error, but passing a lambda to +# default_factory produces the same behavior as default=MISSING and does not raise an +# error. +MissingField = field(default_factory=lambda: MISSING) class _cached_property: @@ -252,6 +264,18 @@ def parse_time(timestamp: str | None) -> datetime.datetime | None: def parse_time(timestamp: str | None) -> datetime.datetime | None: + """A helper function to convert an ISO 8601 timestamp to a datetime object. + + Parameters + ---------- + timestamp: Optional[:class:`str`] + The timestamp to convert. + + Returns + ------- + Optional[:class:`datetime.datetime`] + The converted datetime object. + """ if timestamp: return datetime.datetime.fromisoformat(timestamp) return None @@ -285,7 +309,7 @@ def warn_deprecated( since: Optional[:class:`str`] The version in which the function was deprecated. This should be in the format ``major.minor(.patch)``, where the patch version is optional. - removed: Optional[:class:`str] + removed: Optional[:class:`str`] The version in which the function is planned to be removed. This should be in the format ``major.minor(.patch)``, where the patch version is optional. reference: Optional[:class:`str`] @@ -326,7 +350,7 @@ def deprecated( since: Optional[:class:`str`] The version in which the function was deprecated. This should be in the format ``major.minor(.patch)``, where the patch version is optional. - removed: Optional[:class:`str] + removed: Optional[:class:`str`] The version in which the function is planned to be removed. This should be in the format ``major.minor(.patch)``, where the patch version is optional. reference: Optional[:class:`str`] @@ -408,7 +432,8 @@ def oauth_url( def snowflake_time(id: int) -> datetime.datetime: - """ + """Converts a Discord snowflake ID to a UTC-aware datetime object. + Parameters ---------- id: :class:`int` @@ -542,8 +567,50 @@ def get(iterable: Iterable[T], **attrs: Any) -> T | None: return None -async def get_or_fetch(obj, attr: str, id: int, *, default: Any = MISSING): - # TODO: Document this +async def get_or_fetch(obj, attr: str, id: int, *, default: Any = MISSING) -> Any: + """|coro| + + Attempts to get an attribute from the object in cache. If it fails, it will attempt to fetch it. + If the fetch also fails, an error will be raised. + + Parameters + ---------- + obj: Any + The object to use the get or fetch methods in + attr: :class:`str` + The attribute to get or fetch. Note the object must have both a ``get_`` and ``fetch_`` method for this attribute. + id: :class:`int` + The ID of the object + default: Any + The default value to return if the object is not found, instead of raising an error. + + Returns + ------- + Any + The object found or the default value. + + Raises + ------ + :exc:`AttributeError` + The object is missing a ``get_`` or ``fetch_`` method + :exc:`NotFound` + Invalid ID for the object + :exc:`HTTPException` + An error occurred fetching the object + :exc:`Forbidden` + You do not have permission to fetch the object + + Examples + -------- + + Getting a guild from a guild ID: :: + + guild = await utils.get_or_fetch(client, 'guild', guild_id) + + Getting a channel from the guild. If the channel is not found, return None: :: + + channel = await utils.get_or_fetch(guild, 'channel', channel_id, default=None) + """ getter = getattr(obj, f"get_{attr}")(id) if getter is None: try: @@ -574,7 +641,7 @@ def _get_as_snowflake(data: Any, key: str) -> int | None: def _get_mime_type_for_image(data: bytes): - if data.startswith(b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"): + if data.startswith(b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"): return "image/png" elif data[0:3] == b"\xff\xd8\xff" or data[6:10] in (b"JFIF", b"Exif"): return "image/jpeg" @@ -944,6 +1011,8 @@ def escape_mentions(text: str) -> str: def raw_mentions(text: str) -> list[int]: """Returns a list of user IDs matching ``<@user_id>`` in the string. + .. versionadded:: 2.2 + Parameters ---------- text: :class:`str` @@ -960,6 +1029,8 @@ def raw_mentions(text: str) -> list[int]: def raw_channel_mentions(text: str) -> list[int]: """Returns a list of channel IDs matching ``<@#channel_id>`` in the string. + .. versionadded:: 2.2 + Parameters ---------- text: :class:`str` @@ -976,6 +1047,8 @@ def raw_channel_mentions(text: str) -> list[int]: def raw_role_mentions(text: str) -> list[int]: """Returns a list of role IDs matching ``<@&role_id>`` in the string. + .. versionadded:: 2.2 + Parameters ---------- text: :class:`str` @@ -1032,6 +1105,10 @@ def as_chunks(iterator: _Iter[T], max_size: int) -> _Iter[list[T]]: .. versionadded:: 2.0 + .. warning:: + + The last chunk collected may not be as large as ``max_size``. + Parameters ---------- iterator: Union[:class:`collections.abc.Iterator`, :class:`collections.abc.AsyncIterator`] @@ -1039,10 +1116,6 @@ def as_chunks(iterator: _Iter[T], max_size: int) -> _Iter[list[T]]: max_size: :class:`int` The maximum chunk size. - .. warning:: - - The last chunk collected may not be as large as ``max_size``. - Returns ------- Union[:class:`collections.abc.Iterator`, :class:`collections.abc.AsyncIterator`] diff --git a/discord/voice_client.py b/discord/voice_client.py index edf2d374..110977d2 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -274,12 +274,12 @@ def __init__(self, client: Client, channel: abc.Connectable): @property def guild(self) -> Guild | None: - """Optional[:class:`Guild`]: The guild we're connected to, if applicable.""" + """The guild we're connected to, if applicable.""" return getattr(self.channel, "guild", None) @property def user(self) -> ClientUser: - """:class:`ClientUser`: The user connected to voice (i.e. ourselves).""" + """The user connected to voice (i.e. ourselves).""" return self._state.user def checked_add(self, attr, value, limit): @@ -441,7 +441,7 @@ async def potential_reconnect(self) -> bool: @property def latency(self) -> float: - """:class:`float`: Latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. + """Latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. This could be referred to as the Discord Voice WebSocket latency and is an analogue of user's voice latencies as seen in the Discord client. @@ -453,7 +453,7 @@ def latency(self) -> float: @property def average_latency(self) -> float: - """:class:`float`: Average of most recent 20 HEARTBEAT latencies in seconds. + """Average of most recent 20 HEARTBEAT latencies in seconds. .. versionadded:: 1.4 """ @@ -480,14 +480,16 @@ async def poll_voice_ws(self, reconnect: bool) -> None: break if exc.code == 4014: _log.info( - "Disconnected from voice by force... potentially reconnecting." + "Disconnected from voice by force... potentially" + " reconnecting." ) successful = await self.potential_reconnect() if successful: continue _log.info( - "Reconnect was unsuccessful, disconnecting from voice normally..." + "Reconnect was unsuccessful, disconnecting from voice" + " normally..." ) await self.disconnect() break @@ -543,7 +545,7 @@ async def move_to(self, channel: abc.Snowflake) -> None: await self.channel.guild.change_voice_state(channel=channel) def is_connected(self) -> bool: - """:class:`bool`: Indicates if the voice client is connected to voice.""" + """Indicates if the voice client is connected to voice.""" return self._connected.is_set() # audio related @@ -839,11 +841,11 @@ def recv_decoded_audio(self, data): self.sink.write(data.decoded_data, self.ws.ssrc_map[data.ssrc]["user_id"]) def is_playing(self) -> bool: - """:class:`bool`: Indicates if we're currently playing audio.""" + """Indicates if we're currently playing audio.""" return self._player is not None and self._player.is_playing() def is_paused(self) -> bool: - """:class:`bool`: Indicates if we're playing audio, but if we're paused.""" + """Indicates if we're playing audio, but if we're paused.""" return self._player is not None and self._player.is_paused() def stop(self) -> None: @@ -864,7 +866,7 @@ def resume(self) -> None: @property def source(self) -> AudioSource | None: - """Optional[:class:`AudioSource`]: The audio source being played, if playing. + """The audio source being played, if playing. This property can also be used to change the audio source currently being played. """ diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index f708ddbd..08b77a1b 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -181,7 +181,10 @@ async def request( if remaining == "0" and response.status != 429: delta = utils._parse_ratelimit_header(response) _log.debug( - "Webhook ID %s has been pre-emptively rate limited, waiting %.2f seconds", + ( + "Webhook ID %s has been pre-emptively rate limited," + " waiting %.2f seconds" + ), webhook_id, delta, ) @@ -196,7 +199,10 @@ async def request( retry_after: float = data["retry_after"] # type: ignore _log.warning( - "Webhook ID %s is rate limited. Retrying in %.2f seconds", + ( + "Webhook ID %s is rate limited. Retrying in %.2f" + " seconds" + ), webhook_id, retry_after, ) @@ -744,7 +750,7 @@ def __repr__(self): @property def icon(self) -> Asset | None: - """Optional[:class:`Asset`]: Returns the guild's icon asset, if available.""" + """Returns the guild's icon asset, if available.""" if self._icon is None: return None return Asset._from_guild_icon(self._state, self.id, self._icon) @@ -1001,14 +1007,14 @@ def _update(self, data: WebhookPayload): self.source_guild: PartialWebhookGuild | None = source_guild def is_partial(self) -> bool: - """:class:`bool`: Whether the webhook is a "partial" webhook. + """Whether the webhook is a "partial" webhook. .. versionadded:: 2.0 """ return self.channel_id is None def is_authenticated(self) -> bool: - """:class:`bool`: Whether the webhook is authenticated with a bot token. + """Whether the webhook is authenticated with a bot token. .. versionadded:: 2.0 """ @@ -1016,7 +1022,7 @@ def is_authenticated(self) -> bool: @property def guild(self) -> Guild | None: - """Optional[:class:`Guild`]: The guild this webhook belongs to. + """The guild this webhook belongs to. If this is a partial webhook, then this will always return ``None``. """ @@ -1024,7 +1030,7 @@ def guild(self) -> Guild | None: @property def channel(self) -> TextChannel | None: - """Optional[:class:`TextChannel`]: The text channel this webhook belongs to. + """The text channel this webhook belongs to. If this is a partial webhook, then this will always return ``None``. """ @@ -1033,12 +1039,12 @@ def channel(self) -> TextChannel | None: @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the webhook's creation time in UTC.""" + """Returns the webhook's creation time in UTC.""" return utils.snowflake_time(self.id) @property def avatar(self) -> Asset: - """:class:`Asset`: Returns an :class:`Asset` for the avatar the webhook has. + """Returns an :class:`Asset` for the avatar the webhook has. If the webhook does not have a traditional avatar, an asset for the default avatar is returned instead. @@ -1149,7 +1155,7 @@ def __repr__(self): @property def url(self) -> str: - """:class:`str` : Returns the webhook's url.""" + """Returns the webhook's url.""" return f"https://discord.com/api/webhooks/{self.id}/{self.token}" @classmethod diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 2b0ecc88..042130ac 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -187,7 +187,10 @@ def request( if remaining == "0" and response.status_code != 429: delta = utils._parse_ratelimit_header(response) _log.debug( - "Webhook ID %s has been pre-emptively rate limited, waiting %.2f seconds", + ( + "Webhook ID %s has been pre-emptively rate limited," + " waiting %.2f seconds" + ), webhook_id, delta, ) @@ -202,7 +205,10 @@ def request( retry_after: float = data["retry_after"] # type: ignore _log.warning( - "Webhook ID %s is rate limited. Retrying in %.2f seconds", + ( + "Webhook ID %s is rate limited. Retrying in %.2f" + " seconds" + ), webhook_id, retry_after, ) @@ -626,7 +632,7 @@ def __repr__(self): @property def url(self) -> str: - """:class:`str` : Returns the webhook's url.""" + """Returns the webhook's url.""" return f"https://discord.com/api/webhooks/{self.id}/{self.token}" @classmethod diff --git a/discord/welcome_screen.py b/discord/welcome_screen.py index 841c5d83..ae0a98da 100644 --- a/discord/welcome_screen.py +++ b/discord/welcome_screen.py @@ -127,7 +127,10 @@ def __init__(self, data: WelcomeScreenPayload, guild: Guild): self._update(data) def __repr__(self): - return f" bool: - """:class:`bool`: Indicates whether the welcome screen is enabled or not.""" + """Indicates whether the welcome screen is enabled or not.""" return "WELCOME_SCREEN_ENABLED" in self._guild.features @property def guild(self) -> Guild: - """:class:`Guild`: The guild this welcome screen belongs to.""" + """The guild this welcome screen belongs to.""" return self._guild @overload diff --git a/discord/widget.py b/discord/widget.py index a5780cdc..25784609 100644 --- a/discord/widget.py +++ b/discord/widget.py @@ -89,16 +89,19 @@ def __str__(self) -> str: return self.name def __repr__(self) -> str: - return f"" + return ( + "" + ) @property def mention(self) -> str: - """:class:`str`: The string that allows you to mention the channel.""" + """The string that allows you to mention the channel.""" return f"<#{self.id}>" @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the channel's creation time in UTC.""" + """Returns the channel's creation time in UTC.""" return snowflake_time(self.id) @@ -206,7 +209,7 @@ def __repr__(self) -> str: @property def display_name(self) -> str: - """:class:`str`: Returns the member's display name.""" + """Returns the member's display name.""" return self.nick or self.name @@ -296,17 +299,17 @@ def __repr__(self) -> str: @property def created_at(self) -> datetime.datetime: - """:class:`datetime.datetime`: Returns the member's creation time in UTC.""" + """Returns the member's creation time in UTC.""" return snowflake_time(self.id) @property def json_url(self) -> str: - """:class:`str`: The JSON URL of the widget.""" + """The JSON URL of the widget.""" return f"https://discord.com/api/guilds/{self.id}/widget.json" @property def invite_url(self) -> str: - """Optional[:class:`str`]: The invite URL for the guild, if available.""" + """The invite URL for the guild, if available.""" return self._invite async def fetch_invite(self, *, with_counts: bool = True) -> Invite: