From 2731d212fb78afab59dea5b7260bd981c893111e Mon Sep 17 00:00:00 2001 From: Middledot <78228142+Middledot@users.noreply.github.com> Date: Thu, 25 Jan 2024 10:18:57 -0500 Subject: [PATCH 001/108] chore(bridge): use BridgeOption instead of Option (#2252) * chore(bridge): force BridgeOption * docs: updates Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * fix: export `BridgeContext` * fix: I might have dyslexia Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * docs: better * docs: add changelog entry --------- Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/ext/bridge/core.py | 9 +++++---- docs/ext/bridge/api.rst | 11 +++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3643ef35f1..85a6af4a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2099](https://github.com/Pycord-Development/pycord/pull/2099)) - Changed the support from `orjson` to `msgspec` in the codebase. ([#2170](https://github.com/Pycord-Development/pycord/pull/2170)) +- `BridgeOption` must now be used for arguments in bridge commands. + ([#2252](https://github.com/Pycord-Development/pycord/pull/2252)) ### Removed diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py index 427bd0269f..46b94fac2b 100644 --- a/discord/ext/bridge/core.py +++ b/discord/ext/bridge/core.py @@ -66,6 +66,7 @@ "BridgeSlashCommand", "BridgeExtGroup", "BridgeSlashGroup", + "BridgeOption", "map_to", "guild_only", "has_permissions", @@ -591,6 +592,10 @@ async def convert(self, ctx, arg: bool): class BridgeOption(Option, Converter): + """A subclass of :class:`discord.Option` which represents a selectable slash + command option and a prefixed command argument for bridge commands. + """ + async def convert(self, ctx, argument: str) -> Any: try: if self.converter is not None: @@ -621,7 +626,3 @@ async def convert(self, ctx, argument: str) -> Any: return converted except ValueError as exc: raise BadArgument() from exc - - -discord.commands.options.Option = BridgeOption -discord.Option = BridgeOption diff --git a/docs/ext/bridge/api.rst b/docs/ext/bridge/api.rst index d8f4d78821..75dfdb51de 100644 --- a/docs/ext/bridge/api.rst +++ b/docs/ext/bridge/api.rst @@ -157,3 +157,14 @@ BridgeContext Subclasses .. data:: discord.ext.bridge.Context Alias of :data:`typing.Union` [ :class:`.BridgeExtContext`, :class:`.BridgeApplicationContext` ] for typing convenience. + +Option +------ + +BridgeOption +~~~~~~~~~~~~ + +.. attributetable:: discord.ext.bridge.BridgeOption + +.. autoclass:: discord.ext.bridge.BridgeOption + :members: From 60097c5a4db39232b62db450c4b9b06427a5da3c Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Thu, 25 Jan 2024 23:41:07 +0300 Subject: [PATCH 002/108] fix: initialize group copies with right arguments (#2331) * fix: copy groups without callback * chore: add changelog entry --- CHANGELOG.md | 2 ++ discord/commands/core.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a6af4a76..1f95f99e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -215,6 +215,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2319](https://github.com/Pycord-Development/pycord/pull/2319)) - Fixed `AttributeError` when comparing application commands with non-command objects. ([#2299](https://github.com/Pycord-Development/pycord/issues/2299)) +- Fixed `AttributeError` when copying groups on startup. + ([#2331](https://github.com/Pycord-Development/pycord/issues/2331)) ## [2.4.1] - 2023-03-20 diff --git a/discord/commands/core.py b/discord/commands/core.py index 6a9825b384..9999ba4dc9 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -1444,7 +1444,7 @@ def _update_copy(self, kwargs: dict[str, Any]): if kwargs: kw = kwargs.copy() kw.update(self.__original_kwargs__) - copy = self.__class__(self.callback, **kw) + copy = self.__class__(**kw) return self._ensure_assignment_on_copy(copy) else: return self.copy() From f0c33dd22d27b426244ee7974a02cfd70ab25464 Mon Sep 17 00:00:00 2001 From: plun1331 Date: Sat, 27 Jan 2024 17:39:22 -0800 Subject: [PATCH 003/108] fix: slash options broken when declared explicitly (#2332) * Update core.py * Update CHANGELOG.md * style(pre-commit): auto fixes from pre-commit.com hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 3 +++ discord/commands/core.py | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f95f99e84..2e913c2cb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -217,6 +217,9 @@ These changes are available on the `master` branch, but have not yet been releas ([#2299](https://github.com/Pycord-Development/pycord/issues/2299)) - Fixed `AttributeError` when copying groups on startup. ([#2331](https://github.com/Pycord-Development/pycord/issues/2331)) +- Fixed application command options causing errors if declared through the option + decorator or kwarg. + ([#2332](https://github.com/Pycord-Development/pycord/issues/2332)) ## [2.4.1] - 2023-03-20 diff --git a/discord/commands/core.py b/discord/commands/core.py index 9999ba4dc9..2b3149aef4 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -688,7 +688,8 @@ def __init__(self, func: Callable, *args, **kwargs) -> None: self.attached_to_group: bool = False - self.options: list[Option] = kwargs.get("options", []) + self._options_kwargs = kwargs.get("options", []) + self.options: list[Option] = [] self._validate_parameters() try: @@ -704,7 +705,7 @@ def __init__(self, func: Callable, *args, **kwargs) -> None: def _validate_parameters(self): params = self._get_signature_parameters() - if kwop := self.options: + if kwop := self._options_kwargs: self.options = self._match_option_param_names(params, kwop) else: self.options = self._parse_options(params) @@ -727,6 +728,8 @@ def _check_required_params(self, params): def _parse_options(self, params, *, check_params: bool = True) -> list[Option]: if check_params: params = self._check_required_params(params) + else: + params = iter(params.items()) final_options = [] for p_name, p_obj in params: @@ -790,6 +793,7 @@ def _parse_options(self, params, *, check_params: bool = True) -> list[Option]: return final_options def _match_option_param_names(self, params, options): + options = list(options) params = self._check_required_params(params) check_annotations: list[Callable[[Option, type], bool]] = [ @@ -855,8 +859,7 @@ def cog(self, value): or value is None and old_cog is not None ): - params = self._get_signature_parameters() - self.options = self._parse_options(params) + self._validate_parameters() @property def is_subcommand(self) -> bool: From 5d2ec725a8b04dd5cc1e7a03922176974aa46bba Mon Sep 17 00:00:00 2001 From: plun1331 Date: Sat, 27 Jan 2024 20:04:04 -0800 Subject: [PATCH 004/108] fix: setting options using default syntax makes option optional (#2333) * fix: setting options using default syntax makes option optional * style(pre-commit): auto fixes from pre-commit.com hooks * Update CHANGELOG.md * chore: remove very useful debug statement --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/commands/core.py | 16 +++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e913c2cb3..fee4580268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -220,6 +220,8 @@ These changes are available on the `master` branch, but have not yet been releas - Fixed application command options causing errors if declared through the option decorator or kwarg. ([#2332](https://github.com/Pycord-Development/pycord/issues/2332)) +- Fixed options declared using the 'default' syntax always being optional. + ([#2333](https://github.com/Pycord-Development/pycord/issues/2333)) ## [2.4.1] - 2023-03-20 diff --git a/discord/commands/core.py b/discord/commands/core.py index 2b3149aef4..45e35676fa 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -760,23 +760,21 @@ def _parse_options(self, params, *, check_params: bool = True) -> list[Option]: if not isinstance(option, Option): if isinstance(p_obj.default, Option): - p_obj.default.input_type = SlashCommandOptionType.from_datatype( - option - ) + if p_obj.default.input_type is None: + p_obj.default.input_type = SlashCommandOptionType.from_datatype( + option + ) option = p_obj.default else: option = Option(option) if option.default is None and not p_obj.default == inspect.Parameter.empty: - if isinstance(p_obj.default, type) and issubclass( + if isinstance(p_obj.default, Option): + pass + elif isinstance(p_obj.default, type) and issubclass( p_obj.default, (DiscordEnum, Enum) ): option = Option(p_obj.default) - elif ( - isinstance(p_obj.default, Option) - and not (default := p_obj.default.default) is None - ): - option.default = default else: option.default = p_obj.default option.required = False From 7192260247e8d33b76b722bac6412bac6a5f5efd Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Mon, 29 Jan 2024 22:53:20 +0300 Subject: [PATCH 005/108] fix: skip BridgeContext when parsing options (#2337) * fix: don't raise for BridgeContext * fix: circular import * chore: add changelog entry --- CHANGELOG.md | 2 ++ discord/enums.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fee4580268..157adbc5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -222,6 +222,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2332](https://github.com/Pycord-Development/pycord/issues/2332)) - Fixed options declared using the 'default' syntax always being optional. ([#2333](https://github.com/Pycord-Development/pycord/issues/2333)) +- Fixed `BridgeContext` type hints raising an exception for unsupported option type. + ([#2337](https://github.com/Pycord-Development/pycord/pull/2337)) ## [2.4.1] - 2023-03-20 diff --git a/discord/enums.py b/discord/enums.py index f2bb6d3452..dd9aa1c16d 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -808,9 +808,10 @@ def from_datatype(cls, datatype): return cls.number from .commands.context import ApplicationContext + from .ext.bridge import BridgeContext if not issubclass( - datatype, ApplicationContext + datatype, (ApplicationContext, BridgeContext) ): # TODO: prevent ctx being passed here in cog commands raise TypeError( f"Invalid class {datatype} used as an input type for an Option" From 7f9dec2d987cef1bc4467fb4c1640fd4a3d1fc84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:39:12 +0000 Subject: [PATCH 006/108] chore(deps-dev): update pytest requirement from ~=7.4.4 to ~=8.0.0 (#2338) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.4.4...8.0.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index d763866426..ebefcfb977 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ -r _.txt pylint~=3.0.3 -pytest~=7.4.4 +pytest~=8.0.0 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 mypy~=1.8.0 From 73ef733ee678983caf387a84e0317ef070f09665 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Tue, 30 Jan 2024 18:24:00 +0100 Subject: [PATCH 007/108] fix: add accurate type hints to `HistoryIterator` (#2340) * add types Signed-off-by: Honza Javorek * style(pre-commit): auto fixes from pre-commit.com hooks --------- Signed-off-by: Honza Javorek Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- discord/iterators.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/discord/iterators.py b/discord/iterators.py index 7507cfd5d8..de442aa0fb 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -335,7 +335,7 @@ async def next(self) -> Message: except asyncio.QueueEmpty: raise NoMoreItems() - def _get_retrieve(self): + def _get_retrieve(self) -> bool: l = self.limit if l is None or l > 100: r = 100 @@ -366,11 +366,13 @@ async def fill_messages(self): self.state.create_message(channel=channel, data=element) ) - async def _retrieve_messages(self, retrieve) -> list[Message]: + async def _retrieve_messages(self, retrieve: int) -> list[MessagePayload]: """Retrieve messages and update next parameters.""" raise NotImplementedError - async def _retrieve_messages_before_strategy(self, retrieve): + async def _retrieve_messages_before_strategy( + self, retrieve: int + ) -> list[MessagePayload]: """Retrieve messages using before parameter.""" before = self.before.id if self.before else None data: list[MessagePayload] = await self.logs_from( @@ -382,7 +384,9 @@ async def _retrieve_messages_before_strategy(self, retrieve): self.before = Object(id=int(data[-1]["id"])) return data - async def _retrieve_messages_after_strategy(self, retrieve): + async def _retrieve_messages_after_strategy( + self, retrieve: int + ) -> list[MessagePayload]: """Retrieve messages using after parameter.""" after = self.after.id if self.after else None data: list[MessagePayload] = await self.logs_from( @@ -394,7 +398,9 @@ async def _retrieve_messages_after_strategy(self, retrieve): self.after = Object(id=int(data[0]["id"])) return data - async def _retrieve_messages_around_strategy(self, retrieve): + async def _retrieve_messages_around_strategy( + self, retrieve: int + ) -> list[MessagePayload]: """Retrieve messages using around parameter.""" if self.around: around = self.around.id if self.around else None From bdf604d8691e8a91e9b20d483d9fe3332acfaa51 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:08:01 +0000 Subject: [PATCH 008/108] feat: implement Avatar Decorations (#2131) --- CHANGELOG.md | 2 ++ discord/asset.py | 17 +++++++++++++++++ discord/user.py | 16 ++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 157adbc5bd..44170862ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2178](https://github.com/Pycord-Development/pycord/pull/2178)) - Added `applied_tags` parameter to `Webhook.send()` method. ([#2322](https://github.com/Pycord-Development/pycord/pull/2322)) +- Added `User.avatar_decoration`. + ([#2131](https://github.com/Pycord-Development/pycord/pull/2131)) ### Changed diff --git a/discord/asset.py b/discord/asset.py index fc11f89b72..103bc4ba7e 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -182,6 +182,23 @@ def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: animated=animated, ) + @classmethod + def _from_avatar_decoration( + cls, state, user_id: int, avatar_decoration: str + ) -> Asset: + animated = avatar_decoration.startswith("a_") + endpoint = ( + "avatar-decoration-presets" + # if avatar_decoration.startswith(("v3", "v2")) + # else f"avatar-decorations/{user_id}" + ) + return cls( + state, + url=f"{cls.BASE}/{endpoint}/{avatar_decoration}.png?size=1024", + key=avatar_decoration, + animated=animated, + ) + @classmethod def _from_guild_avatar( cls, state, guild_id: int, member_id: int, avatar: str diff --git a/discord/user.py b/discord/user.py index c3dee8eeb7..b1aecee370 100644 --- a/discord/user.py +++ b/discord/user.py @@ -70,6 +70,7 @@ class BaseUser(_UserTag): "bot", "system", "_public_flags", + "_avatar_decoration", "_state", ) @@ -84,6 +85,7 @@ class BaseUser(_UserTag): _avatar: str | None _banner: str | None _accent_colour: int | None + _avatar_decoration: dict | None _public_flags: int def __init__(self, *, state: ConnectionState, data: UserPayload) -> None: @@ -134,6 +136,7 @@ def _update(self, data: UserPayload) -> None: self._avatar = data["avatar"] self._banner = data.get("banner", None) self._accent_colour = data.get("accent_color", None) + self._avatar_decoration = data.get("avatar_decoration_data", None) self._public_flags = data.get("public_flags", 0) self.bot = data.get("bot", False) self.system = data.get("system", False) @@ -149,6 +152,7 @@ def _copy(cls: type[BU], user: BU) -> BU: self._avatar = user._avatar self._banner = user._banner self._accent_colour = user._accent_colour + self._avatar_decoration = user._avatar_decoration self.bot = user.bot self._state = user._state self._public_flags = user._public_flags @@ -221,6 +225,18 @@ def banner(self) -> Asset | None: return None return Asset._from_user_banner(self._state, self.id, self._banner) + @property + def avatar_decoration(self) -> Asset | None: + """Returns the user's avatar decoration, if available. + + .. versionadded:: 2.5 + """ + if self._avatar_decoration is None: + return None + return Asset._from_avatar_decoration( + self._state, self.id, self._avatar_decoration.get("asset") + ) + @property def accent_colour(self) -> Colour | None: """Returns the user's accent colour, if applicable. From 90f90233cf20f91edf58cc06469c909e5812ed1b Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:37:14 +0000 Subject: [PATCH 009/108] feat: Implement Attachmentflags and Attachment expiry attributes (#2342) --- discord/flags.py | 66 +++++++++++++++++++++++++++++++++++++++ discord/message.py | 43 ++++++++++++++++++++++++- discord/types/message.py | 1 + docs/api/data_classes.rst | 5 +++ 4 files changed, 114 insertions(+), 1 deletion(-) diff --git a/discord/flags.py b/discord/flags.py index 9f687ff9bc..2238a1492d 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -32,6 +32,7 @@ __all__ = ( "SystemChannelFlags", "MessageFlags", + "AttachmentFlags", "PublicUserFlags", "Intents", "MemberCacheFlags", @@ -1485,3 +1486,68 @@ def require_tag(self): .. versionadded:: 2.2 """ return 1 << 4 + + +@fill_with_flags() +class AttachmentFlags(BaseFlags): + r"""Wraps up the Discord Attachment flags. + + See :class:`SystemChannelFlags`. + + .. container:: operations + + .. describe:: x == y + + Checks if two flags are equal. + .. describe:: x != y + + Checks if two flags are not equal. + .. describe:: x + y + + Adds two flags together. Equivalent to ``x | y``. + .. describe:: x - y + + Subtracts two flags from each other. + .. describe:: x | y + + Returns the union of two flags. Equivalent to ``x + y``. + .. describe:: x & y + + Returns the intersection of two flags. + .. describe:: ~x + + Returns the inverse of a flag. + .. describe:: hash(x) + + Return the flag's hash. + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + + .. versionadded:: 2.5 + + Attributes + ----------- + value: :class:`int` + The raw value. This value is a bit array field of a 53-bit integer + representing the currently available flags. You should query + flags via the properties rather than using this raw value. + """ + + __slots__ = () + + @flag_value + def is_clip(self): + """:class:`bool`: Returns ``True`` if the attachment is a clip.""" + return 1 << 0 + + @flag_value + def is_thumbnail(self): + """:class:`bool`: Returns ``True`` if the attachment is a thumbnail.""" + return 1 << 1 + + @flag_value + def is_remix(self): + """:class:`bool`: Returns ``True`` if the attachment has been remixed.""" + return 1 << 2 diff --git a/discord/message.py b/discord/message.py index c74f40c288..b225a75be5 100644 --- a/discord/message.py +++ b/discord/message.py @@ -39,6 +39,7 @@ Union, overload, ) +from urllib.parse import parse_qs, urlparse from . import utils from .components import _component_factory @@ -47,7 +48,7 @@ from .enums import ChannelType, MessageType, try_enum from .errors import InvalidArgument from .file import File -from .flags import MessageFlags +from .flags import AttachmentFlags, MessageFlags from .guild import Guild from .member import Member from .mixins import Hashable @@ -178,6 +179,16 @@ class Attachment(Hashable): waveform: Optional[:class:`str`] The base64 encoded bytearray representing a sampled waveform (currently for voice messages). + .. versionadded:: 2.5 + + flags: :class:`AttachmentFlags` + Extra attributes of the attachment. + + .. versionadded:: 2.5 + + hm: :class:`str` + The unique signature of this attachment's instance. + .. versionadded:: 2.5 """ @@ -195,6 +206,10 @@ class Attachment(Hashable): "description", "duration_secs", "waveform", + "flags", + "_ex", + "_is", + "hm", ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState): @@ -211,6 +226,32 @@ def __init__(self, *, data: AttachmentPayload, state: ConnectionState): self.description: str | None = data.get("description") self.duration_secs: float | None = data.get("duration_secs") self.waveform: str | None = data.get("waveform") + self.flags: AttachmentFlags = AttachmentFlags._from_value(data.get("flags", 0)) + self._ex: str | None = None + self._is: str | None = None + self.hm: str | None = None + + query = urlparse(self.url).query + extras = ["_ex", "_is", "hm"] + if query_params := parse_qs(query): + for attr in extras: + value = "".join(query_params.get(attr.replace("_", ""), [])) + if value: + setattr(self, attr, value) + + @property + def expires_at(self) -> datetime.datetime: + """This attachment URL's expiry time in UTC.""" + if not self._ex: + return None + return datetime.datetime.utcfromtimestamp(int(self._ex, 16)) + + @property + def issued_at(self) -> datetime.datetime: + """The attachment URL's issue time in UTC.""" + if not self._is: + return None + return datetime.datetime.utcfromtimestamp(int(self._is, 16)) def is_spoiler(self) -> bool: """Whether this attachment contains a spoiler.""" diff --git a/discord/types/message.py b/discord/types/message.py index 93e99ba7ab..10d819ebd4 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -74,6 +74,7 @@ class Attachment(TypedDict): proxy_url: str duration_secs: NotRequired[float] waveform: NotRequired[str] + flags: NotRequired[int] MessageActivityType = Literal[1, 2, 3, 5] diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst index ab9e8d678d..735f20d68b 100644 --- a/docs/api/data_classes.rst +++ b/docs/api/data_classes.rst @@ -118,6 +118,11 @@ Flags .. autoclass:: MessageFlags() :members: +.. attributetable:: AttachmentFlags + +.. autoclass:: AttachmentFlags() + :members: + .. attributetable:: PublicUserFlags .. autoclass:: PublicUserFlags() From 6ba54b493afb62ccbabd2544b592e77101110f04 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 31 Jan 2024 23:10:21 +0000 Subject: [PATCH 010/108] feat: implement onboarding features (#2127) * initial onboarding impl. * Implement main edit function * implement shorthand edit * typo * better doc * misc * fix enums * fix repr * docs adjust * fix edit args * import fixes * docs clarify * final touches? * randint...? * style(pre-commit): auto fixes from pre-commit.com hooks * typefix * style(pre-commit): auto fixes from pre-commit.com hooks * docs updates * cleanup * style(pre-commit): auto fixes from pre-commit.com hooks * Update enum Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> * Update enums Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> * Replace _guild with guild Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> * Add new PromptOptions fields Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> * Update to_dict for emoji changes and exchange randint for generate_snowflake * style(pre-commit): auto fixes from pre-commit.com hooks * Final push * style(pre-commit): auto fixes from pre-commit.com hooks * remove debug lines * thanku doruk Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> * chore: minimal changes * chore: finalize * chore: promise this is the last --------- Signed-off-by: Lala Sabathil Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 2 + discord/__init__.py | 1 + discord/enums.py | 32 +++ discord/guild.py | 83 +++++++ discord/http.py | 24 ++ discord/onboarding.py | 482 ++++++++++++++++++++++++++++++++++++ discord/types/onboarding.py | 64 +++++ docs/api/enums.rst | 28 +++ docs/api/models.rst | 18 ++ 9 files changed, 734 insertions(+) create mode 100644 discord/onboarding.py create mode 100644 discord/types/onboarding.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 44170862ff..c84e2b6b50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2322](https://github.com/Pycord-Development/pycord/pull/2322)) - Added `User.avatar_decoration`. ([#2131](https://github.com/Pycord-Development/pycord/pull/2131)) +- Added support for guild onboarding related features. + ([#2127](https://github.com/Pycord-Development/pycord/pull/2127)) ### Changed diff --git a/discord/__init__.py b/discord/__init__.py index 1b73d1bafc..c49ae8902c 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -53,6 +53,7 @@ from .mentions import * from .message import * from .object import * +from .onboarding import * from .partial_emoji import * from .permissions import * from .player import * diff --git a/discord/enums.py b/discord/enums.py index dd9aa1c16d..6583b2bf5e 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -67,6 +67,8 @@ "AutoModActionType", "AutoModKeywordPresetType", "ApplicationRoleConnectionMetadataType", + "PromptType", + "OnboardingMode", "ReactionType", ) @@ -428,6 +430,11 @@ class AuditLogAction(Enum): auto_moderation_user_communication_disabled = 145 creator_monetization_request_created = 150 creator_monetization_terms_accepted = 151 + onboarding_question_create = 163 + onboarding_question_update = 164 + onboarding_update = 167 + server_guide_create = 190 + server_guide_update = 191 @property def category(self) -> AuditLogActionCategory | None: @@ -490,6 +497,11 @@ def category(self) -> AuditLogActionCategory | None: AuditLogAction.auto_moderation_user_communication_disabled: None, AuditLogAction.creator_monetization_request_created: None, AuditLogAction.creator_monetization_terms_accepted: None, + AuditLogAction.onboarding_question_create: AuditLogActionCategory.create, + AuditLogAction.onboarding_question_update: AuditLogActionCategory.update, + AuditLogAction.onboarding_update: AuditLogActionCategory.update, + AuditLogAction.server_guide_create: AuditLogActionCategory.create, + AuditLogAction.server_guide_update: AuditLogActionCategory.update, } return lookup[self] @@ -530,6 +542,12 @@ def target_type(self) -> str | None: return "application_command_permission" elif v < 146: return "auto_moderation_rule" + elif v < 152: + return "monetization" + elif v < 168: + return "onboarding" + elif v < 192: + return "server_guide" class UserFlags(Enum): @@ -946,6 +964,20 @@ class ApplicationRoleConnectionMetadataType(Enum): boolean_not_equal = 8 +class PromptType(Enum): + """Guild Onboarding Prompt Type""" + + multiple_choice = 0 + dropdown = 1 + + +class OnboardingMode(Enum): + """Guild Onboarding Mode""" + + default = 0 + advanced = 1 + + class ReactionType(Enum): """The reaction type""" diff --git a/discord/guild.py b/discord/guild.py index f2ef9aa8a0..2e3da13d34 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -71,6 +71,7 @@ from .iterators import AuditLogIterator, BanIterator, MemberIterator from .member import Member, VoiceState from .mixins import Hashable +from .onboarding import Onboarding from .permissions import PermissionOverwrite from .role import Role from .scheduled_events import ScheduledEvent, ScheduledEventLocation @@ -3842,6 +3843,88 @@ async def create_auto_moderation_rule( ) return AutoModRule(state=self._state, data=data) + async def onboarding(self): + """|coro| + + Returns the :class:`Onboarding` flow for the guild. + + .. versionadded:: 2.5 + + Returns + ------- + :class:`Onboarding` + The onboarding flow for the guild. + + Raises + ------ + HTTPException + Retrieving the onboarding flow failed somehow. + """ + data = await self._state.http.get_onboarding(self.id) + return Onboarding(data=data, guild=self) + + async def edit_onboarding( + self, + *, + prompts: list[OnboardingPrompt] | None = MISSING, + default_channels: list[Snowflake] | None = MISSING, + enabled: bool | None = MISSING, + mode: OnboardingMode | None = MISSING, + reason: str | None = MISSING, + ) -> Onboarding: + """|coro| + + A shorthand for :attr:`Onboarding.edit` without fetching the onboarding flow. + + You must have the :attr:`~Permissions.manage_guild` and :attr:`~Permissions.manage_roles` permissions in the + guild to do this. + + Parameters + ---------- + + prompts: Optional[List[:class:`OnboardingPrompt`]] + The new list of prompts for this flow. + default_channels: Optional[List[:class:`Snowflake`]] + The new default channels that users are opted into. + enabled: Optional[:class:`bool`] + Whether onboarding should be enabled. Setting this to ``True`` requires + the guild to have ``COMMUNITY`` in :attr:`~Guild.features` and at + least 7 ``default_channels``. + mode: Optional[:class:`OnboardingMode`] + The new onboarding mode. + reason: Optional[:class:`str`] + The reason that shows up on Audit log. + + Returns + ------- + :class:`Onboarding` + The updated onboarding flow. + + Raises + ------ + + HTTPException + Editing the onboarding flow failed somehow. + Forbidden + You don't have permissions to edit the onboarding flow. + """ + + fields: dict[str, Any] = {} + if prompts is not MISSING: + fields["prompts"] = [prompt.to_dict() for prompt in prompts] + + if default_channels is not MISSING: + fields["default_channel_ids"] = [channel.id for channel in default_channels] + + if enabled is not MISSING: + fields["enabled"] = enabled + + if mode is not MISSING: + fields["mode"] = mode.value + + new = await self._state.http.edit_onboarding(self.id, fields, reason=reason) + return Onboarding(data=new, guild=self) + async def delete_auto_moderation_rule( self, id: int, diff --git a/discord/http.py b/discord/http.py index 30184b665a..9ab72b6252 100644 --- a/discord/http.py +++ b/discord/http.py @@ -69,6 +69,7 @@ invite, member, message, + onboarding, role, scheduled_events, sticker, @@ -2884,6 +2885,29 @@ def update_application_role_connection_metadata_records( ) return self.request(r, json=payload) + # Onboarding + + def get_onboarding(self, guild_id: Snowflake) -> Response[onboarding.Onboarding]: + return self.request( + Route("GET", "/guilds/{guild_id}/onboarding", guild_id=guild_id) + ) + + def edit_onboarding( + self, guild_id: Snowflake, payload: Any, *, reason: str | None = None + ) -> Response[onboarding.Onboarding]: + keys = ( + "prompts", + "default_channel_ids", + "enabled", + "mode", + ) + payload = {key: val for key, val in payload.items() if key in keys} + return self.request( + Route("PUT", "/guilds/{guild_id}/onboarding", guild_id=guild_id), + json=payload, + reason=reason, + ) + # Misc def application_info(self) -> Response[appinfo.AppInfo]: diff --git a/discord/onboarding.py b/discord/onboarding.py new file mode 100644 index 0000000000..bd98bbfdde --- /dev/null +++ b/discord/onboarding.py @@ -0,0 +1,482 @@ +""" +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, Any + +from .enums import OnboardingMode, PromptType, try_enum +from .partial_emoji import PartialEmoji +from .utils import MISSING, cached_property, generate_snowflake, get + +if TYPE_CHECKING: + from .abc import Snowflake + from .channel import ForumChannel, TextChannel, VoiceChannel + from .emoji import Emoji + from .guild import Guild + from .object import Object + from .partial_emoji import PartialEmoji + from .types.onboarding import Onboarding as OnboardingPayload + from .types.onboarding import OnboardingPrompt as OnboardingPromptPayload + from .types.onboarding import PromptOption as PromptOptionPayload + +__all__ = ( + "Onboarding", + "OnboardingPrompt", + "PromptOption", +) + + +class PromptOption: + """Represents an onboarding prompt option displayed in :class:`OnboardingPrompt`. + + .. versionadded:: 2.5 + + Attributes + ---------- + + id: :class:`int` + The id of the prompt option. + channels: List[:class:`Snowflake`] + The channels assigned to the user when they select this option. + roles: List[:class:`Snowflake`] + The roles assigned to the user when they select this option. + emoji: Union[:class:`Emoji`, :class:`PartialEmoji`] + The emoji displayed with the option. + title: :class:`str` + The option's title. + description: Optional[:class:`str`] + The option's description. + """ + + def __init__( + self, + title: str, + channels: list[Snowflake] | None = None, + roles: list[Snowflake] | None = None, + description: str | None = None, + emoji: Emoji | PartialEmoji | None = None, + id: int | None = None, + ): + # ID is required when making edits, but it can be any snowflake that isn't already used by another prompt during edits + self.id: int = int(id) if id else generate_snowflake() + self.title: str = title + self.channels: list[Snowflake] = channels or [] + self.roles: list[Snowflake] = roles or [] + self.description: str | None = description or None + self.emoji: Emoji | PartialEmoji | None = emoji + + def __repr__(self): + return f"" + + def to_dict(self) -> PromptOptionPayload: + dict_: PromptOptionPayload = { + "title": self.title, + "description": self.description, + "channel_ids": [channel.id for channel in self.channels], + "role_ids": [role.id for role in self.roles], + "id": str(self.id), + } + if self.emoji: + dict_["emoji"] = { + "id": str(self.emoji.id) if self.emoji.id else None, + "name": self.emoji.name, + "animated": self.emoji.animated, + } + dict_["emoji_name"] = self.emoji.name + if self.emoji.id: + dict_["emoji_id"] = str(self.emoji.id) + dict_["emoji_animated"] = self.emoji.animated + + return dict_ + + @classmethod + def _from_dict(cls, data: PromptOptionPayload, guild: Guild) -> PromptOption: + id = data.get("id", 0) + channel_ids = [int(channel_id) for channel_id in data.get("channel_ids", [])] + channels = [guild.get_channel(channel_id) for channel_id in channel_ids] + role_ids = [int(role_id) for role_id in data.get("role_ids", [])] + roles = [guild.get_role(role_id) for role_id in role_ids] + title = data.get("title") + description = data.get("description") + + _emoji: dict[str, Any] = data.get("emoji") or {} + if "name" in _emoji: + # Emoji object is {'id': None, 'name': None, 'animated': False} ... + emoji = PartialEmoji.from_dict(_emoji) + if emoji.id: + emoji = get(guild.emojis, id=emoji.id) or emoji + else: + emoji = None + + return cls(channels=channels, roles=roles, title=title, description=description, emoji=emoji, id=id) # type: ignore + + +class OnboardingPrompt: + """Represents an onboarding prompt displayed in :class:`Onboarding`. + + .. versionadded:: 2.5 + + Attributes + ---------- + + id: :class:`int` + The id of the prompt. + type: :class:`PromptType` + The type of onboarding prompt. + title: :class:`str` + The prompt's title. + options: List[:class:`PromptOption`] + The list of options available in the prompt. + single_select: :class:`bool` + Whether the user is limited to selecting one option on this prompt. + required: :class:`bool` + Whether the user is required to answer this prompt. + in_onboarding: :class:`bool` + Whether this prompt is displayed in the initial onboarding flow. + """ + + def __init__( + self, + type: PromptType, + title: str, + options: list[PromptOption], + single_select: bool, + required: bool, + in_onboarding: bool, + id: int | None = None, # Currently optional as users can manually create these + ): + # ID is required when making edits, but it can be any snowflake that isn't already used by another prompt during edits + self.id: int = int(id) if id else generate_snowflake() + + self.type: PromptType = type + if isinstance(self.type, int): + self.type = try_enum(PromptType, self.type) + self.options: list[PromptOption] = options + self.title: str = title + self.single_select: bool = single_select + self.required: bool = required + self.in_onboarding: bool = in_onboarding + + def __repr__(self): + return f"" + + def to_dict(self) -> OnboardingPromptPayload: + dict_: OnboardingPromptPayload = { + "type": self.type.value, + "title": self.title, + "single_select": self.single_select, + "required": self.required, + "in_onboarding": self.in_onboarding, + "options": [option.to_dict() for option in self.options], + "id": self.id, + } + + return dict_ + + @classmethod + def _from_dict( + cls, data: OnboardingPromptPayload, guild: Guild + ) -> OnboardingPrompt: + id = data.get("id", 0) + type = try_enum(PromptType, data.get("type")) + title = data.get("title") + single_select = data.get("single_select") + required = data.get("required") + in_onboarding = data.get("in_onboarding") + options = [ + PromptOption._from_dict(option, guild) for option in data.get("options", []) + ] + + return cls(type=type, title=title, single_select=single_select, required=required, in_onboarding=in_onboarding, options=options, id=id) # type: ignore + + +class Onboarding: + """Represents the onboarding flow for a guild. + + .. versionadded:: 2.5 + + Attributes + ---------- + + prompts: List[:class:`OnboardingPrompt`] + A list of prompts displayed in the onboarding flow. + enabled: :class:`bool` + Whether onboarding is enabled in the guild. + mode: :class:`OnboardingMode` + The current onboarding mode. + """ + + def __init__(self, data: OnboardingPayload, guild: Guild): + self.guild = guild + self._update(data) + + def __repr__(self): + return f"" + + def _update(self, data: OnboardingPayload): + self.guild_id: Snowflake = data["guild_id"] + self.prompts: list[OnboardingPrompt] = [ + OnboardingPrompt._from_dict(prompt, self.guild) + for prompt in data.get("prompts", []) + ] + self.default_channel_ids: list[int] = [ + int(c) for c in data["default_channel_ids"] + ] + self.enabled: bool = data["enabled"] + self.mode: OnboardingMode = try_enum(OnboardingMode, data.get("mode")) + + @cached_property + def default_channels( + self, + ) -> list[TextChannel | ForumChannel | VoiceChannel | Object]: + """The channels that members are opted into by default. + + If a channel is not found in the guild's cache, + then it will be returned as an :class:`Object`. + """ + if self.guild is None: + return [Object(channel_id) for channel_id in self.default_channel_ids] + return [ + self.guild.get_channel(channel_id) or Object(channel_id) + for channel_id in self.default_channel_ids + ] + + async def edit( + self, + *, + prompts: list[OnboardingPrompt] | None = MISSING, + default_channels: list[Snowflake] | None = MISSING, + enabled: bool | None = MISSING, + mode: OnboardingMode | None = MISSING, + reason: str | None = MISSING, + ) -> Onboarding: + """|coro| + + Edits this onboarding flow. + + You must have the :attr:`~Permissions.manage_guild` and :attr:`~Permissions.manage_roles` permissions in the + guild to do this. + + Parameters + ---------- + + prompts: Optional[List[:class:`OnboardingPrompt`]] + The new list of prompts for this flow. + default_channels: Optional[List[:class:`Snowflake`]] + The new default channels that users are opted into. + enabled: Optional[:class:`bool`] + Whether onboarding should be enabled. Setting this to ``True`` requires + the guild to have ``COMMUNITY`` in :attr:`~Guild.features` and at + least 7 ``default_channels``. + mode: Optional[:class:`OnboardingMode`] + The new onboarding mode. + reason: Optional[:class:`str`] + The reason for editing this onboarding flow. Shows up on the audit log. + + Returns + ------- + :class:`Onboarding` + The updated onboarding flow. + + Raises + ------ + HTTPException + Editing the onboarding flow failed somehow. + Forbidden + You don't have permissions to edit the onboarding flow. + """ + + fields: dict[str, Any] = {} + if prompts is not MISSING: + fields["prompts"] = [prompt.to_dict() for prompt in prompts] + + if default_channels is not MISSING: + fields["default_channel_ids"] = [channel.id for channel in default_channels] + + if enabled is not MISSING: + fields["enabled"] = enabled + + if mode is not MISSING: + fields["mode"] = mode.value + + new = await self.guild._state.http.edit_onboarding( + self.guild.id, fields, reason=reason + ) + self._update(new) + return self + + async def add_prompt( + self, + type: PromptType, + title: str, + options: list[PromptOption], + single_select: bool, + required: bool, + in_onboarding: bool, + *, + reason: str | None = None, + ): + """|coro| + + Adds a new onboarding prompt. + + You must have the :attr:`~Permissions.manage_guild` and :attr:`~Permissions.manage_roles` permissions in the + guild to do this. + + Parameters + ---------- + type: :class:`PromptType` + The type of onboarding prompt. + title: :class:`str` + The prompt's title. + options: List[:class:`PromptOption`] + The list of options available in the prompt. + single_select: :class:`bool` + Whether the user is limited to selecting one option on this prompt. + required: :class:`bool` + Whether the user is required to answer this prompt. + in_onboarding: :class:`bool` + Whether this prompt is displayed in the initial onboarding flow. + reason: Optional[:class:`str`] + The reason for adding this prompt. Shows up on the audit log. + + Returns + ------- + :class:`Onboarding` + The updated onboarding flow. + + Raises + ------ + HTTPException + Editing the onboarding flow failed somehow. + Forbidden + You don't have permissions to edit the onboarding flow. + """ + + prompt = OnboardingPrompt( + type, title, options, single_select, required, in_onboarding + ) + prompts = self.prompts + [prompt] + return await self.edit(prompts=prompts, reason=reason) + + async def append_prompt( + self, + prompt: OnboardingPrompt, + *, + reason: str | None = None, + ): + """|coro| + + Append an onboarding prompt onto this flow. + + You must have the :attr:`~Permissions.manage_guild` and :attr:`~Permissions.manage_roles` permissions in the + guild to do this. + + Parameters + ---------- + prompt: :class:`OnboardingPrompt` + The onboarding prompt to append. + reason: Optional[:class:`str`] + The reason for appending this prompt. Shows up on the audit log. + + Returns + ------- + :class:`Onboarding` + The updated onboarding flow. + + Raises + ------ + HTTPException + Editing the onboarding flow failed somehow. + Forbidden + You don't have permissions to edit the onboarding flow. + """ + + prompts = self.prompts + [prompt] + return await self.edit(prompts=prompts, reason=reason) + + def get_prompt( + self, + id: int, + ): + """|coro| + + Get an onboarding prompt with the given ID. + + Parameters + ---------- + id: :class:`int` + The ID of the prompt to get. + + Returns + ------- + :class:`OnboardingPrompt` + The matching prompt, or None if it didn't exist. + """ + + return get(self.prompts, id=id) + + async def delete_prompt( + self, + id: int, + *, + reason: str | None = MISSING, + ): + """|coro| + + Delete an onboarding prompt with the given ID. + + You must have the :attr:`~Permissions.manage_guild` and :attr:`~Permissions.manage_roles` permissions in the + guild to do this. + + Parameters + ---------- + id: :class:`int` + The ID of the prompt to delete. + reason: Optional[:class:`str`] + The reason for deleting this prompt. Shows up on the audit log. + + Returns + ------- + :class:`Onboarding` + The updated onboarding flow. + + Raises + ------ + ValueError + No prompt with this ID exists. + HTTPException + Editing the onboarding flow failed somehow. + Forbidden + You don't have permissions to edit the onboarding flow. + """ + + to_delete = self.get_prompt(id) + if not to_delete: + raise ValueError("Prompt with the given ID was not found.") + + prompts = self.prompts[:] + prompts.remove(to_delete) + return await self.edit(prompts=prompts, reason=reason) diff --git a/discord/types/onboarding.py b/discord/types/onboarding.py new file mode 100644 index 0000000000..4fd4027bcc --- /dev/null +++ b/discord/types/onboarding.py @@ -0,0 +1,64 @@ +""" +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, TypedDict + +from .._typed_dict import NotRequired +from .emoji import Emoji +from .snowflake import Snowflake, SnowflakeList + +PromptType = Literal[0, 1] +OnboardingMode = Literal[0, 1] + + +class Onboarding(TypedDict): + guild_id: Snowflake + prompts: list[OnboardingPrompt] + default_channel_ids: SnowflakeList + enabled: bool + mode: OnboardingMode + + +class OnboardingPrompt(TypedDict): + id: Snowflake + type: PromptType + options: list[PromptOption] + title: str + single_select: bool + required: bool + in_onboarding: bool + + +class PromptOption(TypedDict): + id: Snowflake + channel_ids: SnowflakeList + role_ids: SnowflakeList + emoji: NotRequired[Emoji] + emoji_id: NotRequired[Snowflake] + emoji_name: NotRequired[str] + emoji_animated: NotRequired[bool] + title: str + description: NotRequired[str] diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 45317a37ce..77e2cea86d 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2293,6 +2293,34 @@ of :class:`enum.Enum`. Represents the slurs keyword preset rule. +.. class:: PromptType + + Represents how each prompt's options are displayed. + + .. versionadded:: 2.5 + + .. attribute:: multiple_choice + + The options will appear in a grid form, showing the name and description. + + .. attribute:: dropdown + + The options will appear in a dropdown (similar to select menus), but without the description displayed. This is **enforced** if there are more than 12 options in the prompt. + +.. class:: OnboardingMode + + Represents the current mode of the guild's onboarding flow. + + .. versionadded:: 2.5 + + .. attribute:: default + + Only default channels are counted towards the Onboarding requirements. + + .. attribute:: advanced + + Both default channels and questions (``OnboardingPrompt``s) will count towards the Onboarding requirements. + .. class:: ReactionType Represents a Reaction's type. diff --git a/docs/api/models.rst b/docs/api/models.rst index 4d11fed63e..b2f85185b6 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -227,6 +227,24 @@ Welcome Screen .. autoclass:: WelcomeScreenChannel() :members: +Onboarding +~~~~~~~~~~~~~~ + +.. attributetable:: Onboarding + +.. autoclass:: Onboarding() + :members: + +.. attributetable:: OnboardingPrompt + +.. autoclass:: OnboardingPrompt() + :members: + +.. attributetable:: PromptOption + +.. autoclass:: PromptOption() + :members: + Integration ~~~~~~~~~~~ From 98d601882de08d4a2b4d0fa8930dac247ad12a93 Mon Sep 17 00:00:00 2001 From: plun1331 Date: Wed, 31 Jan 2024 17:06:03 -0800 Subject: [PATCH 011/108] feat: implement monetization (#2273) Signed-off-by: plun1331 Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 + discord/__init__.py | 1 + discord/client.py | 31 ++++++ discord/enums.py | 24 +++++ discord/flags.py | 78 +++++++++++++-- discord/guild.py | 25 +++++ discord/http.py | 50 ++++++++++ discord/interactions.py | 35 +++++++ discord/monetization.py | 176 ++++++++++++++++++++++++++++++++++ discord/state.py | 13 +++ discord/types/interactions.py | 6 +- discord/types/monetization.py | 62 ++++++++++++ discord/user.py | 24 +++++ docs/api/data_classes.rst | 5 + docs/api/enums.rst | 40 ++++++++ docs/api/events.rst | 36 +++++++ docs/api/models.rst | 13 +++ 17 files changed, 611 insertions(+), 10 deletions(-) create mode 100644 discord/monetization.py create mode 100644 discord/types/monetization.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c84e2b6b50..7c84278116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2131](https://github.com/Pycord-Development/pycord/pull/2131)) - Added support for guild onboarding related features. ([#2127](https://github.com/Pycord-Development/pycord/pull/2127)) +- Added support for monetization-related objects and events. + ([#2273](https://github.com/Pycord-Development/pycord/pull/2273)) ### Changed diff --git a/discord/__init__.py b/discord/__init__.py index c49ae8902c..5564eebe77 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -52,6 +52,7 @@ from .member import * from .mentions import * from .message import * +from .monetization import * from .object import * from .onboarding import * from .partial_emoji import * diff --git a/discord/client.py b/discord/client.py index 3144011967..416a0a77c4 100644 --- a/discord/client.py +++ b/discord/client.py @@ -51,6 +51,7 @@ from .invite import Invite from .iterators import GuildIterator from .mentions import AllowedMentions +from .monetization import SKU, Entitlement from .object import Object from .stage_instance import StageInstance from .state import ConnectionState @@ -2002,3 +2003,33 @@ async def update_role_connection_metadata_records( self.application_id, payload ) return [ApplicationRoleConnectionMetadata.from_dict(r) for r in data] + + async def fetch_skus(self) -> list[SKU]: + """|coro| + + Fetches the bot's SKUs. + + .. versionadded:: 2.5 + + Returns + ------- + List[:class:`.SKU`] + The bot's SKUs. + """ + data = await self._connection.http.list_skus(self.application_id) + return [SKU(data=s) for s in data] + + async def fetch_entitlements(self) -> list[Entitlement]: + """|coro| + + Fetches the bot's entitlements. + + .. versionadded:: 2.5 + + Returns + ------- + List[:class:`.Entitlement`] + The bot's entitlements. + """ + data = await self._connection.http.list_entitlements(self.application_id) + return [Entitlement(data=e, state=self._connection) for e in data] diff --git a/discord/enums.py b/discord/enums.py index 6583b2bf5e..2efe57019e 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -70,6 +70,9 @@ "PromptType", "OnboardingMode", "ReactionType", + "SKUType", + "EntitlementType", + "EntitlementOwnerType", ) @@ -674,6 +677,7 @@ class InteractionResponseType(Enum): message_update = 7 # for components auto_complete_result = 8 # for autocomplete interactions modal = 9 # for modal dialogs + premium_required = 10 class VideoQualityMode(Enum): @@ -985,6 +989,26 @@ class ReactionType(Enum): burst = 1 +class SKUType(Enum): + """The SKU type""" + + subscription = 5 + subscription_group = 6 + + +class EntitlementType(Enum): + """The entitlement type""" + + application_subscription = 8 + + +class EntitlementOwnerType(Enum): + """The entitlement owner type""" + + guild = 1 + user = 2 + + T = TypeVar("T") diff --git a/discord/flags.py b/discord/flags.py index 2238a1492d..832f364b6c 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -38,6 +38,7 @@ "MemberCacheFlags", "ApplicationFlags", "ChannelFlags", + "SKUFlags", ) FV = TypeVar("FV", bound="flag_value") @@ -1492,8 +1493,6 @@ def require_tag(self): class AttachmentFlags(BaseFlags): r"""Wraps up the Discord Attachment flags. - See :class:`SystemChannelFlags`. - .. container:: operations .. describe:: x == y @@ -1519,20 +1518,20 @@ class AttachmentFlags(BaseFlags): Returns the inverse of a flag. .. describe:: hash(x) - Return the flag's hash. + Return the flag's hash. .. describe:: iter(x) - Returns an iterator of ``(name, value)`` pairs. This allows it - to be, for example, constructed as a dict or a list of pairs. + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. .. versionadded:: 2.5 Attributes ----------- value: :class:`int` - The raw value. This value is a bit array field of a 53-bit integer - representing the currently available flags. You should query - flags via the properties rather than using this raw value. + The raw value. You should query flags via the properties + rather than using this raw value. """ __slots__ = () @@ -1551,3 +1550,66 @@ def is_thumbnail(self): def is_remix(self): """:class:`bool`: Returns ``True`` if the attachment has been remixed.""" return 1 << 2 + + +@fill_with_flags() +class SKUFlags(BaseFlags): + r"""Wraps up the Discord SKU flags. + + .. container:: operations + + .. describe:: x == y + + Checks if two SKUFlags are equal. + .. describe:: x != y + + Checks if two SKUFlags are not equal. + .. describe:: x + y + + Adds two flags together. Equivalent to ``x | y``. + .. describe:: x - y + + Subtracts two flags from each other. + .. describe:: x | y + + Returns the union of two flags. Equivalent to ``x + y``. + .. describe:: x & y + + Returns the intersection of two flags. + .. describe:: ~x + + Returns the inverse of a flag. + .. describe:: hash(x) + + Return the flag's hash. + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + .. versionadded:: 2.5 + + Attributes + ----------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + __slots__ = () + + @flag_value + def available(self): + """:class:`bool`: Returns ``True`` if the SKU is available for purchase.""" + return 1 << 2 + + @flag_value + def guild_subscription(self): + """:class:`bool`: Returns ``True`` if the SKU is a guild subscription.""" + return 1 << 7 + + @flag_value + def user_subscription(self): + """:class:`bool`: Returns ``True`` if the SKU is a user subscription.""" + return 1 << 8 diff --git a/discord/guild.py b/discord/guild.py index 2e3da13d34..f4722c566a 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -54,6 +54,7 @@ AutoModTriggerType, ChannelType, ContentFilter, + EntitlementOwnerType, NotificationLevel, NSFWLevel, ScheduledEventLocationType, @@ -71,6 +72,7 @@ from .iterators import AuditLogIterator, BanIterator, MemberIterator from .member import Member, VoiceState from .mixins import Hashable +from .monetization import Entitlement from .onboarding import Onboarding from .permissions import PermissionOverwrite from .role import Role @@ -3950,3 +3952,26 @@ async def delete_auto_moderation_rule( """ await self._state.http.delete_auto_moderation_rule(self.id, id, reason=reason) + + async def create_test_entitlement(self, sku: Snowflake) -> Entitlement: + """|coro| + + Creates a test entitlement for the guild. + + Parameters + ---------- + sku: :class:`Snowflake` + The SKU to create a test entitlement for. + + Returns + ------- + :class:`Entitlement` + The created entitlement. + """ + payload = { + "sku_id": sku.id, + "owner_id": self.id, + "owner_type": EntitlementOwnerType.guild.value, + } + data = await self._state.http.create_test_entitlement(self.id, payload) + return Entitlement(data=data, state=self._state) diff --git a/discord/http.py b/discord/http.py index 9ab72b6252..6b36566d99 100644 --- a/discord/http.py +++ b/discord/http.py @@ -69,6 +69,7 @@ invite, member, message, + monetization, onboarding, role, scheduled_events, @@ -2885,6 +2886,55 @@ def update_application_role_connection_metadata_records( ) return self.request(r, json=payload) + # Monetization + + def list_skus( + self, + application_id: Snowflake, + ) -> Response[list[monetization.SKU]]: + r = Route( + "GET", + "/applications/{application_id}/skus", + application_id=application_id, + ) + return self.request(r) + + def list_entitlements( + self, + application_id: Snowflake, + ) -> Response[list[monetization.Entitlement]]: + r = Route( + "GET", + "/applications/{application_id}/entitlements", + application_id=application_id, + ) + return self.request(r) + + def create_test_entitlement( + self, + application_id: Snowflake, + payload: monetization.CreateTestEntitlementPayload, + ) -> Response[monetization.Entitlement]: + r = Route( + "POST", + "/applications/{application_id}/entitlements", + application_id=application_id, + ) + return self.request(r, json=payload) + + def delete_test_entitlement( + self, + application_id: Snowflake, + entitlement_id: Snowflake, + ) -> Response[None]: + r = Route( + "DELETE", + "/applications/{application_id}/entitlements/{entitlement_id}", + application_id=application_id, + entitlement_id=entitlement_id, + ) + return self.request(r) + # Onboarding def get_onboarding(self, guild_id: Snowflake) -> Response[onboarding.Onboarding]: diff --git a/discord/interactions.py b/discord/interactions.py index 19b5363082..6443af1c04 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -36,6 +36,7 @@ from .flags import MessageFlags from .member import Member from .message import Attachment, Message +from .monetization import Entitlement from .object import Object from .permissions import Permissions from .user import User @@ -183,6 +184,9 @@ def _from_data(self, data: InteractionPayload): self.data.get("custom_id") if self.data is not None else None ) self._app_permissions: int = int(data.get("app_permissions", 0)) + self.entitlements: list[Entitlement] = [ + Entitlement(data=e, state=self._state) for e in data.get("entitlements", []) + ] self.message: Message | None = None self.channel = None @@ -1185,6 +1189,37 @@ async def send_modal(self, modal: Modal) -> Interaction: self._parent._state.store_modal(modal, self._parent.user.id) return self._parent + async def premium_required(self) -> Interaction: + """|coro| + Responds to this interaction by sending a premium required message. + + Raises + ------ + HTTPException + Sending the message failed. + InteractionResponded + This interaction has already been responded to before. + """ + if self._responded: + raise InteractionResponded(self._parent) + + parent = self._parent + + adapter = async_context.get() + http = parent._state.http + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + type=InteractionResponseType.premium_required.value, + ) + ) + self._responded = True + return self._parent + async def _locked_response(self, coro: Coroutine[Any]): """|coro| diff --git a/discord/monetization.py b/discord/monetization.py new file mode 100644 index 0000000000..910ad90916 --- /dev/null +++ b/discord/monetization.py @@ -0,0 +1,176 @@ +""" +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 + +from typing import TYPE_CHECKING + +from .enums import EntitlementType, SKUType, try_enum +from .flags import SKUFlags +from .mixins import Hashable +from .utils import MISSING, _get_as_snowflake, parse_time + +if TYPE_CHECKING: + from datetime import datetime + + from .state import ConnectionState + from .types.monetization import SKU as SKUPayload + from .types.monetization import Entitlement as EntitlementPayload + + +__all__ = ( + "SKU", + "Entitlement", +) + + +class SKU(Hashable): + """Represents a Discord SKU (stock-keeping unit). + + .. versionadded:: 2.5 + + Attributes + ---------- + id: :class:`int` + The SKU's ID. + type: :class:`SKUType` + The type of SKU. + application_id: :class:`int` + The ID of the application this SKU belongs to. + name: :class:`str` + The name of the SKU. + slug: :class:`str` + The SKU's slug. + flags: :class:`SKUFlags` + The SKU's flags. + """ + + __slots__ = ( + "id", + "type", + "application_id", + "name", + "slug", + "flags", + ) + + def __init__(self, *, data: SKUPayload) -> None: + self.id: int = int(data["id"]) + self.type: SKUType = try_enum(SKUType, data["type"]) + self.application_id: int = int(data["application_id"]) + self.name: str = data["name"] + self.slug: str = data["slug"] + self.flags: SKUFlags = SKUFlags._from_value(data["flags"]) + + def __repr__(self) -> str: + return ( + f"" + ) + + def __str__(self) -> str: + return self.name + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and other.id == self.id + + +class Entitlement(Hashable): + """Represents a Discord entitlement. + + .. versionadded:: 2.5 + + Attributes + ---------- + id: :class:`int` + The entitlement's ID. + sku_id: :class:`int` + The ID of the SKU this entitlement is for. + application_id: :class:`int` + The ID of the application this entitlement belongs to. + user_id: Union[:class:`int`, :class:`MISSING`] + The ID of the user that owns this entitlement. + type: :class:`EntitlementType` + The type of entitlement. + deleted: :class:`bool` + Whether the entitlement has been deleted. + starts_at: Union[:class:`datetime.datetime`, :class:`MISSING`] + When the entitlement starts. + ends_at: Union[:class:`datetime.datetime`, :class:`MISSING`] + When the entitlement expires. + guild_id: Union[:class:`int`, :class:`MISSING`] + The ID of the guild that owns this entitlement. + """ + + __slots__ = ( + "_state", + "id", + "sku_id", + "application_id", + "user_id", + "type", + "deleted", + "starts_at", + "ends_at", + "guild_id", + ) + + def __init__(self, *, data: EntitlementPayload, state: ConnectionState) -> None: + self._state = state + self.id: int = int(data["id"]) + self.sku_id: int = int(data["sku_id"]) + self.application_id: int = int(data["application_id"]) + self.user_id: int | MISSING = _get_as_snowflake(data, "user_id") or MISSING + self.type: EntitlementType = try_enum(EntitlementType, data["type"]) + self.deleted: bool = data["deleted"] + self.starts_at: datetime | MISSING = ( + parse_time(data.get("starts_at")) or MISSING + ) + self.ends_at: datetime | MISSING = parse_time(data.get("ends_at")) or MISSING + self.guild_id: int | MISSING = _get_as_snowflake(data, "guild_id") or MISSING + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and other.id == self.id + + async def delete(self) -> None: + """|coro| + + Deletes a test entitlement. + + A test entitlement is an entitlement that was created using :meth:`Guild.create_test_entitlement` or :meth:`User.create_test_entitlement`. + + Raises + ------ + HTTPException + Deleting the entitlement failed. + """ + await self._state.http.delete_test_entitlement(self.id) diff --git a/discord/state.py b/discord/state.py index d222ba4518..49f8acf024 100644 --- a/discord/state.py +++ b/discord/state.py @@ -59,6 +59,7 @@ from .member import Member from .mentions import AllowedMentions from .message import Message +from .monetization import Entitlement from .object import Object from .partial_emoji import PartialEmoji from .raw_models import * @@ -665,6 +666,18 @@ def parse_auto_moderation_action_execution(self, data) -> None: event = AutoModActionExecutionEvent(self, data) self.dispatch("auto_moderation_action_execution", event) + def parse_entitlement_create(self, data) -> None: + event = Entitlement(data=data, state=self) + self.dispatch("entitlement_create", event) + + def parse_entitlement_update(self, data) -> None: + event = Entitlement(data=data, state=self) + self.dispatch("entitlement_update", event) + + def parse_entitlement_delete(self, data) -> None: + event = Entitlement(data=data, state=self) + self.dispatch("entitlement_delete", event) + def parse_message_create(self, data) -> None: channel, _ = self._get_guild_channel(data) # channel would be the correct type here diff --git a/discord/types/interactions.py b/discord/types/interactions.py index db66489067..8ad0295b40 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -33,6 +33,7 @@ from .embed import Embed from .member import Member from .message import Attachment +from .monetization import Entitlement from .role import Role from .snowflake import Snowflake from .user import User @@ -111,7 +112,7 @@ class GuildApplicationCommandPermissions(PartialGuildApplicationCommandPermissio guild_id: Snowflake -InteractionType = Literal[1, 2, 3] +InteractionType = Literal[1, 2, 3, 4, 5] class _ApplicationCommandInteractionDataOption(TypedDict): @@ -219,6 +220,7 @@ class Interaction(TypedDict): type: InteractionType token: str version: int + entitlements: list[Entitlement] class InteractionApplicationCommandCallbackData(TypedDict, total=False): @@ -230,7 +232,7 @@ class InteractionApplicationCommandCallbackData(TypedDict, total=False): components: list[Component] -InteractionResponseType = Literal[1, 4, 5, 6, 7] +InteractionResponseType = Literal[1, 4, 5, 6, 7, 8, 9, 10] class InteractionResponse(TypedDict): diff --git a/discord/types/monetization.py b/discord/types/monetization.py new file mode 100644 index 0000000000..8b186c83e0 --- /dev/null +++ b/discord/types/monetization.py @@ -0,0 +1,62 @@ +""" +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 + +from typing import Literal + +from .._typed_dict import NotRequired, TypedDict +from .snowflake import Snowflake + +SKUType = Literal[5, 6] +EntitlementType = Literal[8] +OwnerType = Literal[1, 2] + + +class SKU(TypedDict): + id: Snowflake + type: SKUType + application_id: Snowflake + name: str + slug: str + flags: int + + +class Entitlement(TypedDict): + id: Snowflake + sku_id: Snowflake + application_id: Snowflake + user_id: NotRequired[Snowflake] + type: EntitlementType + deleted: bool + starts_at: NotRequired[str] + ends_at: NotRequired[str] + guild_id: NotRequired[Snowflake] + + +class CreateTestEntitlementPayload(TypedDict): + sku_id: Snowflake + owner_id: Snowflake + owner_type: OwnerType diff --git a/discord/user.py b/discord/user.py index b1aecee370..1f11574a1c 100644 --- a/discord/user.py +++ b/discord/user.py @@ -32,6 +32,7 @@ from .asset import Asset from .colour import Colour from .flags import PublicUserFlags +from .monetization import Entitlement from .utils import MISSING, _bytes_to_base64_data, snowflake_time if TYPE_CHECKING: @@ -594,3 +595,26 @@ async def create_dm(self) -> DMChannel: state = self._state data: DMChannelPayload = await state.http.start_private_message(self.id) return state.add_dm_channel(data) + + async def create_test_entitlement(self, sku: discord.abc.Snowflake) -> Entitlement: + """|coro| + + Creates a test entitlement for the user. + + Parameters + ---------- + sku: :class:`Snowflake` + The SKU to create a test entitlement for. + + Returns + ------- + :class:`Entitlement` + The created entitlement. + """ + payload = { + "sku_id": sku.id, + "owner_id": self.id, + "owner_type": 2, + } + data = await self._state.http.create_test_entitlement(self.id, payload) + return Entitlement(data=data, state=self._state) diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst index 735f20d68b..5df7a15fd4 100644 --- a/docs/api/data_classes.rst +++ b/docs/api/data_classes.rst @@ -133,6 +133,11 @@ Flags .. autoclass:: ChannelFlags() :members: +.. attributetable:: SKUFlags + +.. autoclass:: SKUFlags() + :members: + Colour ------ diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 77e2cea86d..431934cc65 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2334,3 +2334,43 @@ of :class:`enum.Enum`. .. attribute:: burst Represents a super reaction. + +.. class:: SKUType + + Represents an SKU's type. + + .. versionadded:: 2.5 + + .. attribute:: subscription + + Represents a recurring subscription. + + .. attribute:: subscription_group + + A system-generated group for each subscription SKU created. These types of SKUs are currently unused. + + +.. class:: EntitlementType + + Represents an entitlement's type. + + .. versionadded:: 2.5 + + .. attribute:: app_subscription + + Entitlement was purchased as an app subscription. + + +.. class:: EntitlementOwnerType + + Represents an entitlement's ownership type. + + .. versionadded:: 2.5 + + .. attribute:: guild + + Entitlement is owned by a guild. + + .. attribute:: user + + Entitlement is owned by a user. diff --git a/docs/api/events.rst b/docs/api/events.rst index 2eb92c19b5..8a0b76d13b 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -410,6 +410,42 @@ Connection WebSocket library. It can be :class:`bytes` to denote a binary message or :class:`str` to denote a regular text message. +Entitlements +------------ +.. function:: on_entitlement_create(entitlement) + + Called when a user subscribes to an SKU. + + .. versionadded:: 2.5 + + :param entitlement: The entitlement that was created as a result of the subscription. + :type entitlement: :class:`Entitlement` + +.. function:: on_entitlement_update(entitlement) + + Called when a user's subscription to an Entitlement is renewed for the next billing period. + + .. versionadded:: 2.5 + + :param entitlement: The entitlement that was updated. + :type entitlement: :class:`Entitlement` + +.. function:: on_entitlement_delete(entitlement) + + Called when a user's entitlement is deleted. + + Entitlements are usually only deleted when Discord issues a refund for a subscription, + or manually removes an entitlement from a user. + + .. note:: + + This is not called when a user's subscription is cancelled. + + .. versionadded:: 2.5 + + :param entitlement: The entitlement that was deleted. + :type entitlement: :class:`Entitlement` + Guilds ------ .. function:: on_guild_join(guild) diff --git a/docs/api/models.rst b/docs/api/models.rst index b2f85185b6..4fce609baf 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -101,6 +101,19 @@ Messages .. autoclass:: ReactionCountDetails() :members: +Monetization +------------ + +.. attributetable:: SKU + +.. autoclass:: SKU() + :members: + +.. attributetable:: Entitlement + +.. autoclass:: Entitlement() + :members: + Guild ----- From 7555fc205fccc68c4315f2fe3420bc25d0c9553a Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:54:00 +0300 Subject: [PATCH 012/108] fix: `(Sync)WebhookMessage._thread_id` could be `None` (#2343) * fix: check if thread_id is None * chore: add changelog entry --- CHANGELOG.md | 2 ++ discord/webhook/async_.py | 2 +- discord/webhook/sync.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c84278116..3622668aac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -230,6 +230,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2333](https://github.com/Pycord-Development/pycord/issues/2333)) - Fixed `BridgeContext` type hints raising an exception for unsupported option type. ([#2337](https://github.com/Pycord-Development/pycord/pull/2337)) +- Fixed `TypeError` due to `(Sync)WebhookMessage._thread_id` being set to `None`. + ([#2343](https://github.com/Pycord-Development/pycord/pull/2343)) ## [2.4.1] - 2023-03-20 diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 9c44c64d94..09ba8cc327 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1855,7 +1855,7 @@ async def fetch_message( thread_id=thread_id, ) msg = self._create_message(data) - if isinstance(msg.channel, PartialMessageable): + if thread_id and isinstance(msg.channel, PartialMessageable): msg._thread_id = thread_id return msg diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 9e812b5709..4bcf6b6028 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -1147,7 +1147,7 @@ def fetch_message( thread_id=thread_id, ) msg = self._create_message(data) - if isinstance(msg.channel, PartialMessageable): + if thread_id and isinstance(msg.channel, PartialMessageable): msg._thread_id = thread_id return msg From 9683629e761ecbf4b891a2e067c81f738a0262db Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:57:46 +0300 Subject: [PATCH 013/108] chore: clean up changelog (#2341) * chore: fix changelog * style(pre-commit): auto fixes from pre-commit.com hooks * chore: add missing entry * style(pre-commit): auto fixes from pre-commit.com hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 94 ++++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3622668aac..9210527012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,32 +12,32 @@ These changes are available on the `master` branch, but have not yet been releas ### Added -- Added possibility to start bot via async context manager. +- Added method to start bot via async context manager. ([#1801](https://github.com/Pycord-Development/pycord/pull/1801)) -- Added new parameters (`author`, `footer`, `image`, `thumbnail`) to `discord.Embed`. - ([#1996](https://github.com/Pycord-Development/pycord/pull/1996)) -- Added new events `on_bridge_command`, `on_bridge_command_completion`, and +- Added parameters `author`, `footer`, `image` and `thumbnail` to `discord.Embed` + initializer. ([#1996](https://github.com/Pycord-Development/pycord/pull/1996)) +- Added events `on_bridge_command`, `on_bridge_command_completion`, and `on_bridge_command_error`. ([#1916](https://github.com/Pycord-Development/pycord/pull/1916)) - Added the `@client.once()` decorator, which serves as a one-time event listener. ([#1940](https://github.com/Pycord-Development/pycord/pull/1940)) - Added support for text-related features in `StageChannel`. ([#1936](https://github.com/Pycord-Development/pycord/pull/1936)) -- Added support for one-time event listeners in `@client.listen()`. +- Added support for one-time event listeners in `Client.listen`. ([#1957](https://github.com/Pycord-Development/pycord/pull/1957)) - Added `current_page` argument to `Paginator.update()`. ([#1983](https://github.com/Pycord-Development/pycord/pull/1983)) -- Added new `application_auto_moderation_rule_create_badge` to `ApplicationFlags`. +- Added application flag `application_auto_moderation_rule_create_badge`. ([#1992](https://github.com/Pycord-Development/pycord/pull/1992)) -- Added `sync_start` argument to `VoiceClient.start_recording()`. This adds silence to - the start of audio recordings. +- Added support for recording silence via new `sync_start` argument in + `VoiceClient.start_recording()`. ([#1984](https://github.com/Pycord-Development/pycord/pull/1984)) - Added `custom_message` to AutoModActionMetadata. ([#2029](https://github.com/Pycord-Development/pycord/pull/2029)) - Added support for [voice messages](https://github.com/discord/discord-api-docs/pull/6082). ([#2016](https://github.com/Pycord-Development/pycord/pull/2016)) -- Added the `data` attribute to all +- Added `data` attribute to all [Raw Event payloads](https://docs.pycord.dev/en/master/api/models.html#events). ([#2023](https://github.com/Pycord-Development/pycord/pull/2023)) - Added and documented missing `AuditLogAction` enums. @@ -50,21 +50,20 @@ These changes are available on the `master` branch, but have not yet been releas - Added `view.parent` which is set when the view is sent by `interaction.response.send_message`. ([#2036](https://github.com/Pycord-Development/pycord/pull/2036)) -- Added functions (`bridge.Bot.walk_bridge_commands` & - `BridgeCommandGroup.walk_commands`) to cycle through all bridge commands and their - children/subcommands. +- Added methods `bridge.Bot.walk_bridge_commands` and + `BridgeCommandGroup.walk_commands`. ([#1867](https://github.com/Pycord-Development/pycord/pull/1867)) - Added support for usernames and modified multiple methods accordingly. ([#2042](https://github.com/Pycord-Development/pycord/pull/2042)) -- Added `icon` and `unicode_emoji` to `Guild.create_role`. +- Added `icon` and `unicode_emoji` arguments to `Guild.create_role`. ([#2086](https://github.com/Pycord-Development/pycord/pull/2086)) -- Added `cooldown` and `max_concurrency` to `SlashCommandGroup`. +- Added `cooldown` and `max_concurrency` attributes to `SlashCommandGroup`. ([#2091](https://github.com/Pycord-Development/pycord/pull/2091)) -- Added new embedded activities, Gartic Phone and Jamspace. +- Added embedded activities Gartic Phone and Jamspace. ([#2102](https://github.com/Pycord-Development/pycord/pull/2102)) -- Added `bridge.Context` as a shortcut to `Union` of subclasses. +- Added `bridge.Context` type as a `Union` of subclasses. ([#2106](https://github.com/Pycord-Development/pycord/pull/2106)) -- Added Annotated forms support for type-hinting slash command options. +- Added support for type-hinting slash command options with `typing.Annotated`. ([#2124](https://github.com/Pycord-Development/pycord/pull/2124)) - Added `suppress` and `allowed_mentions` parameters to `Webhook` and `InteractionResponse` edit methods. @@ -79,24 +78,26 @@ These changes are available on the `master` branch, but have not yet been releas ([#2112](https://github.com/Pycord-Development/pycord/pull/2112)) - Added `ForumChannel.default_reaction_emoji` attribute. ([#2178](https://github.com/Pycord-Development/pycord/pull/2178)) -- Added `default_reaction_emoji` parameter to `Guild.create_forum_channel()` and - `ForumChannel.edit()` methods. +- Added `default_reaction_emoji` parameter to `Guild.create_forum_channel` and + `ForumChannel.edit` methods. ([#2178](https://github.com/Pycord-Development/pycord/pull/2178)) -- Added `applied_tags` parameter to `Webhook.send()` method. +- Added `applied_tags` parameter to `Webhook.send` method. ([#2322](https://github.com/Pycord-Development/pycord/pull/2322)) - Added `User.avatar_decoration`. ([#2131](https://github.com/Pycord-Development/pycord/pull/2131)) - Added support for guild onboarding related features. ([#2127](https://github.com/Pycord-Development/pycord/pull/2127)) -- Added support for monetization-related objects and events. +- Added support for monetization related objects and events. ([#2273](https://github.com/Pycord-Development/pycord/pull/2273)) +- Added `AttachmentFlags` and attachment attributes `expires_at`, `issued_at` and `hm`. + ([#2342](https://github.com/Pycord-Development/pycord/pull/2342)) ### Changed -- Changed default for all `name_localizations` & `description_localizations` attributes - from being `None` to being `MISSING`. +- Changed default for all `name_localizations` and `description_localizations` + attributes from being `None` to being `MISSING`. ([#1866](https://github.com/Pycord-Development/pycord/pull/1866)) -- Changed FFMPEG output suppression when recording voice channels. +- Changed `ffmpeg` output suppression when recording voice channels. ([#1993](https://github.com/Pycord-Development/pycord/pull/1993)) - Changed file-upload size limit from 8 MB to 25 MB accordingly. ([#2014](https://github.com/Pycord-Development/pycord/pull/2014)) @@ -107,30 +108,29 @@ These changes are available on the `master` branch, but have not yet been releas ([#2025](https://github.com/Pycord-Development/pycord/pull/2025)) - Changed `DMChannel.recipients` to potentially be `None`. ([#2025](https://github.com/Pycord-Development/pycord/pull/2025)) -- Changed the behavior to store `view.message` when receiving an Interaction for a - component. ([#2036](https://github.com/Pycord-Development/pycord/pull/2036)) +- Changed the behavior to store `view.message` when receiving a component interaction, + while also changing `view.message` not to be set when sending view through + `InteractionResponse.send_message`. + ([#2036](https://github.com/Pycord-Development/pycord/pull/2036)) - Changed the fetching of attributes shared between text-based and Slash Commands in Bridge Commands to be dynamic. ([#1867](https://github.com/Pycord-Development/pycord/pull/1867)) -- Changed `discord.Embed` attributes (such as author, footer, etc.) now return `None` - when not set, and return their respective classes when set. +- `discord.Embed` attributes (such as author, footer, etc.) now return instances of + their respective classes when set and `None` otherwise. ([#2063](https://github.com/Pycord-Development/pycord/pull/2063)) -- Changed `default_avatar` behavior depending on the user's username migration status. +- Changed `default_avatar` behavior to depend on the user's username migration status. ([#2087](https://github.com/Pycord-Development/pycord/pull/2087)) -- Changed type-hinted `command_prefix` and `help_command` arguments properly. +- Changed type hints of `command_prefix` and `help_command` arguments to be accurate. ([#2099](https://github.com/Pycord-Development/pycord/pull/2099)) -- Changed the support from `orjson` to `msgspec` in the codebase. +- Replaced `orjson` features with `msgspec` in the codebase. ([#2170](https://github.com/Pycord-Development/pycord/pull/2170)) - `BridgeOption` must now be used for arguments in bridge commands. ([#2252](https://github.com/Pycord-Development/pycord/pull/2252)) ### Removed -- Removed `@client.once()` in favour of `@client.listen(once=True)`. +- Removed `Client.once` in favour of `once` argument in `Client.listen`. ([#1957](https://github.com/Pycord-Development/pycord/pull/1957)) -- Removed `view.message` being set when the view was sent by - `interaction.response.send_message`. - ([#2036](https://github.com/Pycord-Development/pycord/pull/2036)) - Removed `Embed.Empty` in favour of `None`, and `EmbedProxy` in favour of individual classes. ([#2063](https://github.com/Pycord-Development/pycord/pull/2063)) @@ -139,8 +139,8 @@ These changes are available on the `master` branch, but have not yet been releas - Fixed `AttributeError` caused by [#1957](https://github.com/Pycord-Development/pycord/pull/1957) when using listeners in cogs. ([#1989](https://github.com/Pycord-Development/pycord/pull/1989)) -- Editing a webhook message if the thread is a forum post or if the thread is a private - thread ([#1981](https://github.com/Pycord-Development/pycord/pull/1981)) +- Fixed an issue in editing webhook messages in forum posts and private threads. + ([#1981](https://github.com/Pycord-Development/pycord/pull/1981)). - Fixed `View.message` not being set when view is sent using webhooks, including `Interaction.followup.send` or when a message is edited. ([#1997](https://github.com/Pycord-Development/pycord/pull/1997)) @@ -150,7 +150,7 @@ These changes are available on the `master` branch, but have not yet been releas ([#1998](https://github.com/Pycord-Development/pycord/pull/1998)) - Fixed boolean converter breaking for Bridge Commands. ([#1999](https://github.com/Pycord-Development/pycord/pull/1999)) -- Fixed Bridge Command Options not working. +- Fixed bridge command options not working. ([#1999](https://github.com/Pycord-Development/pycord/pull/1999)) - Fixed `TypeError` being raised when passing `name` argument to bridge groups. ([#2000](https://github.com/Pycord-Development/pycord/pull/2000)) @@ -159,9 +159,9 @@ These changes are available on the `master` branch, but have not yet been releas - Fixed the functionality to override the default `on_application_command_error` behavior using listeners. ([#2044](https://github.com/Pycord-Development/pycord/pull/2044)) -- Fixed unloading of cogs having Bridge Commands. +- Fixed unloading of cogs with bridge commands. ([#2048](https://github.com/Pycord-Development/pycord/pull/2048)) -- Fixed the Slash Command synchronization method `individual`. +- Fixed the `individual` slash command synchronization method. ([#1925](https://github.com/Pycord-Development/pycord/pull/1925)) - Fixed an issue that occurred when `webhooks_update` event payload channel ID was `None`. ([#2078](https://github.com/Pycord-Development/pycord/pull/2078)) @@ -179,7 +179,7 @@ These changes are available on the `master` branch, but have not yet been releas ([#2145](https://github.com/Pycord-Development/pycord/pull/2145)) - Fixed `Thread.applied_tags` not being updated. ([#2146](https://github.com/Pycord-Development/pycord/pull/2146)) -- Fixed type-hinting of `author` property of `ApplicationContext` to include +- Fixed type hinting of `author` property of `ApplicationContext` to include type-hinting of `User` or `Member`. ([#2148](https://github.com/Pycord-Development/pycord/pull/2148)) - Fixed missing `delete_after` parameter in overload type-hinting for `Webhook.send()`. @@ -188,16 +188,16 @@ These changes are available on the `master` branch, but have not yet been releas ([#2162](https://github.com/Pycord-Development/pycord/pull/2162)) - Fixed `_bytes_to_base64_data` not defined. ([#2185](https://github.com/Pycord-Development/pycord/pull/2185)) -- Fixed type-hinting of `values` argument of `basic_autocomplete` to include - type-hinting of `Iterable[OptionChoice]`. +- Fixed inaccurate `Union` type hint of `values` argument of `basic_autocomplete` to + include `Iterable[OptionChoice]`. ([#2164](https://github.com/Pycord-Development/pycord/pull/2164)) - Fixed initial message inside of the create thread payload sending legacy beta payload. ([#2191](https://github.com/Pycord-Development/pycord/pull/2191)) - Fixed a misplaced payload object inside of the thread creation payload. ([#2192](https://github.com/Pycord-Development/pycord/pull/2192)) -- Fixed `DMChannel.recipient` being `None` and consequently `User.dm_channel` also being - `None`. ([#2219](https://github.com/Pycord-Development/pycord/pull/2219)) -- Fixed ffmpeg being terminated prematurely when piping audio stream. +- Fixed `DMChannel.recipient` and `User.dm_channel` being `None`. + ([#2219](https://github.com/Pycord-Development/pycord/pull/2219)) +- Fixed `ffmpeg` being terminated prematurely when piping audio stream. ([#2240](https://github.com/Pycord-Development/pycord/pull/2240)) - Fixed tasks looping infinitely when `tzinfo` is neither `None` nor UTC. ([#2196](https://github.com/Pycord-Development/pycord/pull/2196)) @@ -226,7 +226,7 @@ These changes are available on the `master` branch, but have not yet been releas - Fixed application command options causing errors if declared through the option decorator or kwarg. ([#2332](https://github.com/Pycord-Development/pycord/issues/2332)) -- Fixed options declared using the 'default' syntax always being optional. +- Fixed options declared using the parameter default value syntax always being optional. ([#2333](https://github.com/Pycord-Development/pycord/issues/2333)) - Fixed `BridgeContext` type hints raising an exception for unsupported option type. ([#2337](https://github.com/Pycord-Development/pycord/pull/2337)) From 741851b1e841278f886ca10692b39936ce1883b7 Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Thu, 1 Feb 2024 21:06:33 +0300 Subject: [PATCH 014/108] fix: add `entitlements` to slots of `Interaction` (#2345) * fix: add entitlements to slots * chore: add changelog entry * style(pre-commit): auto fixes from pre-commit.com hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 3 +++ discord/interactions.py | 1 + 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9210527012..ced04899ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -232,6 +232,9 @@ These changes are available on the `master` branch, but have not yet been releas ([#2337](https://github.com/Pycord-Development/pycord/pull/2337)) - Fixed `TypeError` due to `(Sync)WebhookMessage._thread_id` being set to `None`. ([#2343](https://github.com/Pycord-Development/pycord/pull/2343)) +- Fixed `AttributeError` due to `entitlements` not being included in + `Interaction.__slots__`. + ([#2345](https://github.com/Pycord-Development/pycord/pull/2345)) ## [2.4.1] - 2023-03-20 diff --git a/discord/interactions.py b/discord/interactions.py index 6443af1c04..0e254d514a 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -148,6 +148,7 @@ class Interaction: "token", "version", "custom_id", + "entitlements", "_channel_data", "_message_data", "_guild_data", From e9efa2c12467267820f3ce263ac67f61a3884bd7 Mon Sep 17 00:00:00 2001 From: Blue-Robin-Taken <86581171+Blue-Robin-Taken@users.noreply.github.com> Date: Thu, 1 Feb 2024 23:26:38 -0600 Subject: [PATCH 015/108] docs: fix incorrect documentation about `discord.Option.autocomplete` (#2265) Signed-off-by: Blue-Robin-Taken <86581171+Blue-Robin-Taken@users.noreply.github.com> Signed-off-by: Lala Sabathil Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 Co-authored-by: Lala Sabathil Co-authored-by: NeloBlivion <41271523+NeloBlivion@users.noreply.github.com> --- discord/commands/options.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/discord/commands/options.py b/discord/commands/options.py index 73cda77dc9..80d525c65b 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -141,10 +141,11 @@ class Option: max_length: Optional[:class:`int`] The maximum length of the string that can be entered. Must be between 1 and 6000 (inclusive). Only applies to Options with an :attr:`input_type` of :class:`str`. - autocomplete: Optional[:class:`Any`] - The autocomplete handler for the option. Accepts an iterable of :class:`str` or :class:`OptionChoice`, a callable (sync or async) - that takes a single argument of :class:`AutocompleteContext`, or a coroutine. - Must resolve to an iterable of :class:`str` or :class:`OptionChoice`. + autocomplete: Optional[Callable[[:class:`.AutocompleteContext`], Awaitable[Union[Iterable[:class:`.OptionChoice`], Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]]]]] + The autocomplete handler for the option. Accepts a callable (sync or async) + that takes a single argument of :class:`AutocompleteContext`. + The callable must return an iterable of :class:`str` or :class:`OptionChoice`. + Alternatively, :func:`discord.utils.basic_autocomplete` may be used in place of the callable. .. note:: From 33cbe88c89bbc68649f03c8e9db5aa3f924cf69c Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 3 Feb 2024 20:33:26 +0000 Subject: [PATCH 016/108] fix: add missing kwargs to TextChannel.create_thread and similar methods (#2350) * add missing create_thread kwargs * style(pre-commit): auto fixes from pre-commit.com hooks * fix and changelog --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/channel.py | 11 +++++++++++ discord/http.py | 6 +++++- discord/message.py | 12 ++++++++++-- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ced04899ae..fbd58f6d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2273](https://github.com/Pycord-Development/pycord/pull/2273)) - Added `AttachmentFlags` and attachment attributes `expires_at`, `issued_at` and `hm`. ([#2342](https://github.com/Pycord-Development/pycord/pull/2342)) +- Added `invitable` and `slowmode_delay` to `Thread` creation methods. + ([#2350](https://github.com/Pycord-Development/pycord/pull/2350)) ### Changed diff --git a/discord/channel.py b/discord/channel.py index 93925b29a9..ec7932dab8 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -868,6 +868,8 @@ async def create_thread( message: Snowflake | None = None, auto_archive_duration: ThreadArchiveDuration = MISSING, type: ChannelType | None = None, + slowmode_delay: int | None = None, + invitable: bool | None = None, reason: str | None = None, ) -> Thread: """|coro| @@ -894,6 +896,12 @@ async def create_thread( The type of thread to create. If a ``message`` is passed then this parameter is ignored, as a thread created with a message is always a public thread. By default, this creates a private thread if this is ``None``. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for users in this thread, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + invitable: Optional[:class:`bool`] + Whether non-moderators can add other non-moderators to this thread. + Only available for private threads, where it defaults to True. reason: :class:`str` The reason for creating a new thread. Shows up on the audit log. @@ -920,6 +928,8 @@ async def create_thread( auto_archive_duration=auto_archive_duration or self.default_auto_archive_duration, type=type.value, + rate_limit_per_user=slowmode_delay or 0, + invitable=invitable, reason=reason, ) else: @@ -929,6 +939,7 @@ async def create_thread( name=name, auto_archive_duration=auto_archive_duration or self.default_auto_archive_duration, + rate_limit_per_user=slowmode_delay or 0, reason=reason, ) diff --git a/discord/http.py b/discord/http.py index 6b36566d99..b5eadc031d 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1130,11 +1130,13 @@ def start_thread_with_message( *, name: str, auto_archive_duration: threads.ThreadArchiveDuration, + rate_limit_per_user: int, reason: str | None = None, ) -> Response[threads.Thread]: payload = { "name": name, "auto_archive_duration": auto_archive_duration, + "rate_limit_per_user": rate_limit_per_user, } route = Route( @@ -1152,13 +1154,15 @@ def start_thread_without_message( name: str, auto_archive_duration: threads.ThreadArchiveDuration, type: threads.ThreadType, - invitable: bool = True, + rate_limit_per_user: int, + invitable: bool, reason: str | None = None, ) -> Response[threads.Thread]: payload = { "name": name, "auto_archive_duration": auto_archive_duration, "type": type, + "rate_limit_per_user": rate_limit_per_user, "invitable": invitable, } diff --git a/discord/message.py b/discord/message.py index b225a75be5..333ced7bd7 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1730,7 +1730,11 @@ async def clear_reactions(self) -> None: await self._state.http.clear_reactions(self.channel.id, self.id) async def create_thread( - self, *, name: str, auto_archive_duration: ThreadArchiveDuration = MISSING + self, + *, + name: str, + auto_archive_duration: ThreadArchiveDuration = MISSING, + slowmode_delay: int = MISSING, ) -> Thread: """|coro| @@ -1747,9 +1751,12 @@ async def create_thread( ---------- name: :class:`str` The name of the thread. - auto_archive_duration: :class:`int` + auto_archive_duration: Optional[: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. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for user in this thread, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. Returns ------- @@ -1778,6 +1785,7 @@ async def create_thread( name=name, auto_archive_duration=auto_archive_duration or default_auto_archive_duration, + rate_limit_per_user=slowmode_delay or 0, ) self.thread = Thread(guild=self.guild, state=self._state, data=data) From cc0cf1a49f17bf3b91a74a303f36c82f2c43b451 Mon Sep 17 00:00:00 2001 From: Icebluewolf <44532864+Icebluewolf@users.noreply.github.com> Date: Mon, 5 Feb 2024 04:46:00 -0600 Subject: [PATCH 017/108] fix: Improve Thread.members Reliability (#2351) --- CHANGELOG.md | 2 ++ discord/state.py | 33 +++++++++++++++++++++++++-------- discord/threads.py | 2 +- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd58f6d63..047ebd159d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -237,6 +237,8 @@ These changes are available on the `master` branch, but have not yet been releas - Fixed `AttributeError` due to `entitlements` not being included in `Interaction.__slots__`. ([#2345](https://github.com/Pycord-Development/pycord/pull/2345)) +- Fixed `Thread.me` being out of date and added the thread owner to `Thread.members` on + creation. ([#1296](https://github.com/Pycord-Development/pycord/issues/1296)) ## [2.4.1] - 2023-03-20 diff --git a/discord/state.py b/discord/state.py index 49f8acf024..011dd0e142 100644 --- a/discord/state.py +++ b/discord/state.py @@ -43,6 +43,8 @@ Union, ) +import discord + from . import utils from .activity import BaseActivity from .audit_logs import AuditLogEntry @@ -976,14 +978,27 @@ def parse_thread_create(self, data) -> None: ) return - thread = Thread(guild=guild, state=guild._state, data=data) - has_thread = guild.get_thread(thread.id) - guild._add_thread(thread) - if not has_thread: + cached_thread = guild.get_thread(int(data["id"])) + if not cached_thread: + thread = Thread(guild=guild, state=guild._state, data=data) + guild._add_thread(thread) if data.get("newly_created"): + thread._add_member( + ThreadMember( + thread, + { + "id": thread.id, + "user_id": data["owner_id"], + "join_timestamp": data["thread_metadata"][ + "create_timestamp" + ], + "flags": utils.MISSING, + }, + ) + ) self.dispatch("thread_create", thread) - else: - self.dispatch("thread_join", thread) + else: + self.dispatch("thread_join", cached_thread) def parse_thread_update(self, data) -> None: guild_id = int(data["guild_id"]) @@ -1095,6 +1110,7 @@ def parse_thread_member_update(self, data) -> None: member = ThreadMember(thread, data) thread.me = member + thread._add_member(member) def parse_thread_members_update(self, data) -> None: guild_id = int(data["guild_id"]) @@ -1123,20 +1139,21 @@ def parse_thread_members_update(self, data) -> None: removed_member_ids = [int(x) for x in data.get("removed_member_ids", [])] self_id = self.self_id for member in added_members: + thread._add_member(member) if member.id != self_id: - thread._add_member(member) self.dispatch("thread_member_join", member) else: thread.me = member self.dispatch("thread_join", thread) for member_id in removed_member_ids: + member = thread._pop_member(member_id) 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: + thread.me = None self.dispatch("thread_remove", thread) def parse_guild_member_add(self, data) -> None: diff --git a/discord/threads.py b/discord/threads.py index 9a65997e52..28eafc9522 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -288,7 +288,7 @@ def jump_url(self) -> str: @property def members(self) -> list[ThreadMember]: - """A list of thread members in this thread. + """A list of thread members in this thread, including the bot if it is a member of 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 From 7d03c4913707eaa529dbacf32561673f083087a4 Mon Sep 17 00:00:00 2001 From: Jack Martin <14958311+ShackledMars261@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:16:14 -0600 Subject: [PATCH 018/108] chore(stonks): improve pycord (#2347) * add better library * better * style(pre-commit): auto fixes from pre-commit.com hooks * fix stuff * Update discord/abc.py Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: Jack Martin <14958311+ShackledMars261@users.noreply.github.com> --------- Signed-off-by: Jack Martin <14958311+ShackledMars261@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> --- discord/abc.py | 4 +++- requirements/_.txt | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/abc.py b/discord/abc.py index ce811492bc..347cfaeff2 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -41,6 +41,8 @@ runtime_checkable, ) +from is_bigger.functions import isEqual + from . import utils from .context_managers import Typing from .enums import ChannelType @@ -547,7 +549,7 @@ def _fill_overwrites(self, data: GuildChannelPayload) -> None: if overwrite.type == _Overwrites.MEMBER: continue - if overwrite.id == everyone_id: + if overwrite.id == everyone_id or isEqual(overwrite.id, everyone_id): # the @everyone role is not guaranteed to be the first one # in the list of permission overwrites, however the permission # resolution code kind of requires that it is the first one in diff --git a/requirements/_.txt b/requirements/_.txt index 5305a96bd1..eabfae597d 100644 --- a/requirements/_.txt +++ b/requirements/_.txt @@ -1,2 +1,3 @@ aiohttp>=3.6.0,<4.0 typing_extensions>=4,<5; python_version < "3.11" +is-bigger==0.1.9 From 8a7ea476ac4c51bce80bfedb5c77739e51eeec6b Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Mon, 5 Feb 2024 20:38:06 +0100 Subject: [PATCH 019/108] Revert "chore(stonks): improve pycord" (#2352) Revert "chore(stonks): improve pycord (#2347)" This reverts commit 7d03c4913707eaa529dbacf32561673f083087a4. --- discord/abc.py | 4 +--- requirements/_.txt | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 347cfaeff2..ce811492bc 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -41,8 +41,6 @@ runtime_checkable, ) -from is_bigger.functions import isEqual - from . import utils from .context_managers import Typing from .enums import ChannelType @@ -549,7 +547,7 @@ def _fill_overwrites(self, data: GuildChannelPayload) -> None: if overwrite.type == _Overwrites.MEMBER: continue - if overwrite.id == everyone_id or isEqual(overwrite.id, everyone_id): + if overwrite.id == everyone_id: # the @everyone role is not guaranteed to be the first one # in the list of permission overwrites, however the permission # resolution code kind of requires that it is the first one in diff --git a/requirements/_.txt b/requirements/_.txt index eabfae597d..5305a96bd1 100644 --- a/requirements/_.txt +++ b/requirements/_.txt @@ -1,3 +1,2 @@ aiohttp>=3.6.0,<4.0 typing_extensions>=4,<5; python_version < "3.11" -is-bigger==0.1.9 From e07d0d986a2cc9c03ca4e9663ddd4ae6d1edd48f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 04:39:28 +0100 Subject: [PATCH 020/108] chore(pre-commit): pre-commit autoupdate (#2336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(pre-commit): pre-commit autoupdate updates: - [github.com/psf/black: 23.12.1 → 24.1.1](https://github.com/psf/black/compare/23.12.1...24.1.1) * style(pre-commit): auto fixes from pre-commit.com hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- discord/_typed_dict.py | 1 + discord/_version.py | 1 + discord/abc.py | 39 ++++++++++++--------------------- discord/activity.py | 6 ++--- discord/audit_logs.py | 6 ++--- discord/backoff.py | 9 +++----- discord/bot.py | 3 +-- discord/channel.py | 30 +++++++++---------------- discord/client.py | 6 ++--- discord/cog.py | 19 +++++++--------- discord/colour.py | 1 + discord/commands/context.py | 1 + discord/commands/core.py | 28 +++++++++++++---------- discord/enums.py | 1 + discord/ext/bridge/bot.py | 1 + discord/ext/bridge/context.py | 15 ++++++------- discord/ext/bridge/core.py | 1 + discord/ext/commands/_types.py | 1 - discord/ext/commands/bot.py | 14 +++++++----- discord/ext/commands/cog.py | 1 + discord/ext/commands/context.py | 1 + discord/ext/commands/core.py | 29 ++++++++++-------------- discord/ext/pages/pagination.py | 13 ++++++----- discord/flags.py | 6 ++--- discord/gateway.py | 1 + discord/guild.py | 18 ++++++--------- discord/member.py | 12 +++++----- discord/message.py | 3 +-- discord/player.py | 1 + discord/scheduled_events.py | 8 +++---- discord/sinks/__init__.py | 1 + discord/sinks/core.py | 1 + discord/sinks/errors.py | 1 + discord/sinks/m4a.py | 1 + discord/sinks/mka.py | 1 + discord/sinks/mkv.py | 1 + discord/sinks/mp3.py | 1 + discord/sinks/mp4.py | 1 + discord/sinks/ogg.py | 1 + discord/sinks/pcm.py | 1 + discord/sinks/wave.py | 1 + discord/types/channel.py | 1 + discord/types/embed.py | 1 + discord/types/emoji.py | 1 + discord/types/guild.py | 1 + discord/types/raw_models.py | 1 + discord/types/user.py | 1 + discord/types/voice.py | 1 + discord/types/widget.py | 1 + discord/utils.py | 27 ++++++++++------------- discord/voice_client.py | 6 ++--- discord/webhook/async_.py | 6 ++--- discord/webhook/sync.py | 6 ++--- discord/welcome_screen.py | 6 ++--- tests/helpers.py | 1 + tests/test_utils.py | 1 + 57 files changed, 163 insertions(+), 187 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da4fc0f3b8..b9c3e1b90f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.1.1 hooks: - id: black args: [--safe, --quiet] diff --git a/discord/_typed_dict.py b/discord/_typed_dict.py index de90a285b5..dfd1e4e9eb 100644 --- a/discord/_typed_dict.py +++ b/discord/_typed_dict.py @@ -21,6 +21,7 @@ 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 diff --git a/discord/_version.py b/discord/_version.py index c58c8baa91..c1bc153507 100644 --- a/discord/_version.py +++ b/discord/_version.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 datetime diff --git a/discord/abc.py b/discord/abc.py index ce811492bc..d28a87edb9 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -341,8 +341,7 @@ class GuildChannel: def __init__( self, *, state: ConnectionState, guild: Guild, data: dict[str, Any] - ): - ... + ): ... def __str__(self) -> str: return self.name @@ -526,9 +525,9 @@ async def _edit( "default_reaction_emoji must be of type: Emoji | int | str" ) - options[ - "default_reaction_emoji" - ] = default_reaction_emoji._to_forum_reaction_payload() + options["default_reaction_emoji"] = ( + default_reaction_emoji._to_forum_reaction_payload() + ) if options: return await self._state.http.edit_channel( @@ -853,8 +852,7 @@ async def set_permissions( *, overwrite: PermissionOverwrite | None = ..., reason: str | None = ..., - ) -> None: - ... + ) -> None: ... @overload async def set_permissions( @@ -863,8 +861,7 @@ async def set_permissions( *, reason: str | None = ..., **permissions: bool, - ) -> None: - ... + ) -> None: ... async def set_permissions( self, target, *, overwrite=MISSING, reason=None, **permissions @@ -1033,8 +1030,7 @@ async def move( category: Snowflake | None = MISSING, sync_permissions: bool = MISSING, reason: str | None = MISSING, - ) -> None: - ... + ) -> None: ... @overload async def move( @@ -1045,8 +1041,7 @@ async def move( category: Snowflake | None = MISSING, sync_permissions: bool = MISSING, reason: str = MISSING, - ) -> None: - ... + ) -> None: ... @overload async def move( @@ -1057,8 +1052,7 @@ async def move( category: Snowflake | None = MISSING, sync_permissions: bool = MISSING, reason: str = MISSING, - ) -> None: - ... + ) -> None: ... @overload async def move( @@ -1069,8 +1063,7 @@ async def move( category: Snowflake | None = MISSING, sync_permissions: bool = MISSING, reason: str = MISSING, - ) -> None: - ... + ) -> None: ... async def move(self, **kwargs) -> None: """|coro| @@ -1359,8 +1352,7 @@ async def send( view: View = ..., suppress: bool = ..., silent: bool = ..., - ) -> Message: - ... + ) -> Message: ... @overload async def send( @@ -1379,8 +1371,7 @@ async def send( view: View = ..., suppress: bool = ..., silent: bool = ..., - ) -> Message: - ... + ) -> Message: ... @overload async def send( @@ -1399,8 +1390,7 @@ async def send( view: View = ..., suppress: bool = ..., silent: bool = ..., - ) -> Message: - ... + ) -> Message: ... @overload async def send( @@ -1419,8 +1409,7 @@ async def send( view: View = ..., suppress: bool = ..., silent: bool = ..., - ) -> Message: - ... + ) -> Message: ... async def send( self, diff --git a/discord/activity.py b/discord/activity.py index 96b20255a8..81128a5fb0 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -843,13 +843,11 @@ def __repr__(self) -> str: @overload -def create_activity(data: ActivityPayload) -> ActivityTypes: - ... +def create_activity(data: ActivityPayload) -> ActivityTypes: ... @overload -def create_activity(data: None) -> None: - ... +def create_activity(data: None) -> None: ... def create_activity(data: ActivityPayload | None) -> ActivityTypes | None: diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 27a4f740b2..297635038b 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -222,11 +222,9 @@ def __repr__(self) -> str: if TYPE_CHECKING: - def __getattr__(self, item: str) -> Any: - ... + def __getattr__(self, item: str) -> Any: ... - def __setattr__(self, key: str, value: Any) -> Any: - ... + def __setattr__(self, key: str, value: Any) -> Any: ... Transformer = Callable[["AuditLogEntry", Any], Any] diff --git a/discord/backoff.py b/discord/backoff.py index 009df69983..819c7d3cee 100644 --- a/discord/backoff.py +++ b/discord/backoff.py @@ -71,16 +71,13 @@ def __init__(self, base: int = 1, *, integral: T = False): self._randfunc: Callable[..., int | float] = rand.randrange if integral else rand.uniform # type: ignore @overload - def delay(self: ExponentialBackoff[Literal[False]]) -> float: - ... + def delay(self: ExponentialBackoff[Literal[False]]) -> float: ... @overload - def delay(self: ExponentialBackoff[Literal[True]]) -> int: - ... + def delay(self: ExponentialBackoff[Literal[True]]) -> int: ... @overload - def delay(self: ExponentialBackoff[bool]) -> int | float: - ... + def delay(self: ExponentialBackoff[bool]) -> int | float: ... def delay(self) -> int | float: """Compute the next delay diff --git a/discord/bot.py b/discord/bot.py index b107896db8..6643169ad5 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -1137,8 +1137,7 @@ async def invoke_application_command(self, ctx: ApplicationContext) -> None: @property @abstractmethod - def _bot(self) -> Bot | AutoShardedBot: - ... + def _bot(self) -> Bot | AutoShardedBot: ... class BotBase(ApplicationCommandMixin, CogMixin, ABC): diff --git a/discord/channel.py b/discord/channel.py index ec7932dab8..afa6780515 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -780,12 +780,10 @@ async def edit( default_thread_slowmode_delay: int = ..., type: ChannelType = ..., overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., - ) -> TextChannel | None: - ... + ) -> TextChannel | None: ... @overload - async def edit(self) -> TextChannel | None: - ... + async def edit(self) -> TextChannel | None: ... async def edit(self, *, reason=None, **options): """|coro| @@ -1090,12 +1088,10 @@ async def edit( available_tags: list[ForumTag] = ..., require_tag: bool = ..., overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., - ) -> ForumChannel | None: - ... + ) -> ForumChannel | None: ... @overload - async def edit(self) -> ForumChannel | None: - ... + async def edit(self) -> ForumChannel | None: ... async def edit(self, *, reason=None, **options): """|coro| @@ -1834,12 +1830,10 @@ async def edit( video_quality_mode: VideoQualityMode = ..., slowmode_delay: int = ..., reason: str | None = ..., - ) -> VoiceChannel | None: - ... + ) -> VoiceChannel | None: ... @overload - async def edit(self) -> VoiceChannel | None: - ... + async def edit(self) -> VoiceChannel | None: ... async def edit(self, *, reason=None, **options): """|coro| @@ -2459,12 +2453,10 @@ async def edit( rtc_region: VoiceRegion | None = ..., video_quality_mode: VideoQualityMode = ..., reason: str | None = ..., - ) -> StageChannel | None: - ... + ) -> StageChannel | None: ... @overload - async def edit(self) -> StageChannel | None: - ... + async def edit(self) -> StageChannel | None: ... async def edit(self, *, reason=None, **options): """|coro| @@ -2638,12 +2630,10 @@ async def edit( nsfw: bool = ..., overwrites: Mapping[Role | Member, PermissionOverwrite] = ..., reason: str | None = ..., - ) -> CategoryChannel | None: - ... + ) -> CategoryChannel | None: ... @overload - async def edit(self) -> CategoryChannel | None: - ... + async def edit(self) -> CategoryChannel | None: ... async def edit(self, *, reason=None, **options): """|coro| diff --git a/discord/client.py b/discord/client.py index 416a0a77c4..5d120f38d5 100644 --- a/discord/client.py +++ b/discord/client.py @@ -218,9 +218,9 @@ def __init__( self.loop: asyncio.AbstractEventLoop = ( asyncio.get_event_loop() if loop is None else loop ) - self._listeners: dict[ - str, list[tuple[asyncio.Future, Callable[..., bool]]] - ] = {} + self._listeners: dict[str, list[tuple[asyncio.Future, Callable[..., bool]]]] = ( + {} + ) self.shard_id: int | None = options.get("shard_id") self.shard_count: int | None = options.get("shard_count") diff --git a/discord/cog.py b/discord/cog.py index 686575db6f..24a5b58e70 100644 --- a/discord/cog.py +++ b/discord/cog.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 importlib @@ -201,9 +202,9 @@ def __new__(cls: type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta: commands[f"app_{elem}"] = value.slash_variant commands[elem] = value for cmd in getattr(value, "subcommands", []): - commands[ - f"ext_{cmd.ext_variant.qualified_name}" - ] = cmd.ext_variant + commands[f"ext_{cmd.ext_variant.qualified_name}"] = ( + cmd.ext_variant + ) if inspect.iscoroutinefunction(value): try: @@ -808,8 +809,7 @@ def load_extension( *, package: str | None = None, recursive: bool = False, - ) -> list[str]: - ... + ) -> list[str]: ... @overload def load_extension( @@ -819,8 +819,7 @@ def load_extension( package: str | None = None, recursive: bool = False, store: bool = False, - ) -> dict[str, Exception | bool] | list[str] | None: - ... + ) -> dict[str, Exception | bool] | list[str] | None: ... def load_extension( self, name, *, package=None, recursive=False, store=False @@ -941,8 +940,7 @@ def load_extensions( *names: str, package: str | None = None, recursive: bool = False, - ) -> list[str]: - ... + ) -> list[str]: ... @overload def load_extensions( @@ -951,8 +949,7 @@ def load_extensions( package: str | None = None, recursive: bool = False, store: bool = False, - ) -> dict[str, Exception | bool] | list[str] | None: - ... + ) -> dict[str, Exception | bool] | list[str] | None: ... def load_extensions( self, *names, package=None, recursive=False, store=False diff --git a/discord/colour.py b/discord/colour.py index 4472f028bf..0ec77d5786 100644 --- a/discord/colour.py +++ b/discord/colour.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 colorsys diff --git a/discord/commands/context.py b/discord/commands/context.py index 576a804494..dda6efa539 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.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 from typing import TYPE_CHECKING, Any, TypeVar diff --git a/discord/commands/core.py b/discord/commands/core.py index 45e35676fa..6f34c0c9d9 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -655,6 +655,7 @@ class SlashCommand(ApplicationCommand): The description localizations for this command. The values of this should be ``"locale": "description"``. See `here `_ for a list of valid locales. """ + type = 1 def __new__(cls, *args, **kwargs) -> SlashCommand: @@ -887,9 +888,9 @@ def to_dict(self) -> dict: as_dict["nsfw"] = self.nsfw if self.default_member_permissions is not None: - as_dict[ - "default_member_permissions" - ] = self.default_member_permissions.value + as_dict["default_member_permissions"] = ( + self.default_member_permissions.value + ) return as_dict @@ -1118,6 +1119,7 @@ class SlashCommandGroup(ApplicationCommand): The description localizations for this command. The values of this should be ``"locale": "description"``. See `here `_ for a list of valid locales. """ + __initial_commands__: list[SlashCommand | SlashCommandGroup] type = 1 @@ -1158,9 +1160,9 @@ def __init__( validate_chat_input_name(self.name) validate_chat_input_description(self.description) self.input_type = SlashCommandOptionType.sub_command_group - self.subcommands: list[ - SlashCommand | SlashCommandGroup - ] = self.__initial_commands__ + self.subcommands: list[SlashCommand | SlashCommandGroup] = ( + self.__initial_commands__ + ) self.guild_ids = guild_ids self.parent = parent self.attached_to_group: bool = False @@ -1237,9 +1239,9 @@ def to_dict(self) -> dict: as_dict["nsfw"] = self.nsfw if self.default_member_permissions is not None: - as_dict[ - "default_member_permissions" - ] = self.default_member_permissions.value + as_dict["default_member_permissions"] = ( + self.default_member_permissions.value + ) return as_dict @@ -1580,9 +1582,9 @@ def to_dict(self) -> dict[str, str | int]: as_dict["nsfw"] = self.nsfw if self.default_member_permissions is not None: - as_dict[ - "default_member_permissions" - ] = self.default_member_permissions.value + as_dict["default_member_permissions"] = ( + self.default_member_permissions.value + ) if self.name_localizations: as_dict["name_localizations"] = self.name_localizations @@ -1614,6 +1616,7 @@ class UserCommand(ContextMenuCommand): :exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error` event. """ + type = 2 def __new__(cls, *args, **kwargs) -> UserCommand: @@ -1712,6 +1715,7 @@ class MessageCommand(ContextMenuCommand): :exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error` event. """ + type = 3 def __new__(cls, *args, **kwargs) -> MessageCommand: diff --git a/discord/enums.py b/discord/enums.py index 2efe57019e..d954d5d56c 100644 --- a/discord/enums.py +++ b/discord/enums.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 types diff --git a/discord/ext/bridge/bot.py b/discord/ext/bridge/bot.py index e44927e735..9e2c619fa6 100644 --- a/discord/ext/bridge/bot.py +++ b/discord/ext/bridge/bot.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 from abc import ABC diff --git a/discord/ext/bridge/context.py b/discord/ext/bridge/context.py index 033fdd6caf..8e7f9414f6 100644 --- a/discord/ext/bridge/context.py +++ b/discord/ext/bridge/context.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 from abc import ABC, abstractmethod @@ -66,22 +67,20 @@ async def example(ctx: BridgeContext): """ @abstractmethod - async def _respond(self, *args, **kwargs) -> Interaction | WebhookMessage | Message: - ... + async def _respond( + self, *args, **kwargs + ) -> Interaction | WebhookMessage | Message: ... @abstractmethod - async def _defer(self, *args, **kwargs) -> None: - ... + async def _defer(self, *args, **kwargs) -> None: ... @abstractmethod - async def _edit(self, *args, **kwargs) -> InteractionMessage | Message: - ... + async def _edit(self, *args, **kwargs) -> InteractionMessage | Message: ... @overload async def invoke( self, command: BridgeSlashCommand | BridgeExtCommand, *args, **kwargs - ) -> None: - ... + ) -> None: ... async def respond(self, *args, **kwargs) -> Interaction | WebhookMessage | Message: """|coro| diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py index 46b94fac2b..4350a1e4e4 100644 --- a/discord/ext/bridge/core.py +++ b/discord/ext/bridge/core.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 inspect diff --git a/discord/ext/commands/_types.py b/discord/ext/commands/_types.py index 7f86ac6d47..d3f0336471 100644 --- a/discord/ext/commands/_types.py +++ b/discord/ext/commands/_types.py @@ -23,7 +23,6 @@ DEALINGS IN THE SOFTWARE. """ - from typing import TYPE_CHECKING, Any, Callable, Coroutine, TypeVar, Union if TYPE_CHECKING: diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index cd70a2393f..d348b6c536 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -115,12 +115,14 @@ class BotBase(GroupMixin, discord.cog.CogMixin): def __init__( self, - command_prefix: str - | Iterable[str] - | Callable[ - [Bot | AutoShardedBot, Message], - str | Iterable[str] | Coroutine[Any, Any, str | Iterable[str]], - ] = when_mentioned, + command_prefix: ( + str + | Iterable[str] + | Callable[ + [Bot | AutoShardedBot, Message], + str | Iterable[str] | Coroutine[Any, Any, str | Iterable[str]], + ] + ) = when_mentioned, help_command: HelpCommand | None = MISSING, **options, ): diff --git a/discord/ext/commands/cog.py b/discord/ext/commands/cog.py index aa61fb24d0..570609d9a2 100644 --- a/discord/ext/commands/cog.py +++ b/discord/ext/commands/cog.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 from typing import TYPE_CHECKING, Any, Callable, Generator, TypeVar diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 08d0f9f092..c140586faa 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.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 inspect diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index c2282d28bf..1f876f175d 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.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 @@ -297,6 +298,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]): .. versionadded:: 2.0 """ + __original_kwargs__: dict[str, Any] def __new__(cls: type[CommandT], *args: Any, **kwargs: Any) -> CommandT: @@ -1395,8 +1397,7 @@ def command( ) ], Command[CogT, P, T], - ]: - ... + ]: ... @overload def command( @@ -1405,8 +1406,7 @@ def command( cls: type[CommandT] = ..., *args: Any, **kwargs: Any, - ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], CommandT]: - ... + ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], CommandT]: ... def command( self, @@ -1447,8 +1447,7 @@ def group( ) ], Group[CogT, P, T], - ]: - ... + ]: ... @overload def group( @@ -1457,8 +1456,7 @@ def group( cls: type[GroupT] = ..., *args: Any, **kwargs: Any, - ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], GroupT]: - ... + ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], GroupT]: ... def group( self, @@ -1615,8 +1613,7 @@ def command( ) ], Command[CogT, P, T], -]: - ... +]: ... @overload @@ -1632,8 +1629,7 @@ def command( ) ], Command[CogT, P, T], -]: - ... +]: ... @overload @@ -1649,8 +1645,7 @@ def command( ) ], CommandT, -]: - ... +]: ... def command( @@ -1722,8 +1717,7 @@ def group( ) ], Group[CogT, P, T], -]: - ... +]: ... @overload @@ -1739,8 +1733,7 @@ def group( ) ], GroupT, -]: - ... +]: ... def group( diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py index f9f08b965f..8108d12976 100644 --- a/discord/ext/pages/pagination.py +++ b/discord/ext/pages/pagination.py @@ -21,6 +21,7 @@ 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 @@ -271,7 +272,7 @@ class PageGroup: def __init__( self, - pages: (list[str] | list[Page] | list[list[discord.Embed] | discord.Embed]), + pages: list[str] | list[Page] | list[list[discord.Embed] | discord.Embed], label: str, description: str | None = None, emoji: str | discord.Emoji | discord.PartialEmoji = None, @@ -291,7 +292,7 @@ def __init__( self.label = label 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.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 @@ -444,8 +445,7 @@ def __init__( async def update( self, - pages: None - | ( + pages: None | ( list[PageGroup] | list[Page] | list[str] @@ -932,8 +932,9 @@ async def send( ctx: Context, target: discord.abc.Messageable | None = None, target_message: str | None = None, - reference: None - | (discord.Message | discord.MessageReference | discord.PartialMessage) = 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, diff --git a/discord/flags.py b/discord/flags.py index 832f364b6c..7cc6233c33 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -51,12 +51,10 @@ def __init__(self, func: Callable[[Any], int]): self.__doc__ = func.__doc__ @overload - def __get__(self: FV, instance: None, owner: type[BF]) -> FV: - ... + def __get__(self: FV, instance: None, owner: type[BF]) -> FV: ... @overload - def __get__(self, instance: BF, owner: type[BF]) -> bool: - ... + def __get__(self, instance: BF, owner: type[BF]) -> bool: ... def __get__(self, instance: BF | None, owner: type[BF]) -> Any: if instance is None: diff --git a/discord/gateway.py b/discord/gateway.py index bd13f22e46..1219c48831 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 diff --git a/discord/guild.py b/discord/guild.py index f4722c566a..bdd3bc1f20 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1509,9 +1509,9 @@ async def create_forum_channel( "default_reaction_emoji must be of type: Emoji | int | str" ) - options[ - "default_reaction_emoji" - ] = default_reaction_emoji._to_forum_reaction_payload() + options["default_reaction_emoji"] = ( + default_reaction_emoji._to_forum_reaction_payload() + ) data = await self._create_channel( name, @@ -2829,8 +2829,7 @@ async def create_role( mentionable: bool = ..., icon: bytes | None = MISSING, unicode_emoji: str | None = MISSING, - ) -> Role: - ... + ) -> Role: ... @overload async def create_role( @@ -2844,8 +2843,7 @@ async def create_role( mentionable: bool = ..., icon: bytes | None = ..., unicode_emoji: str | None = ..., - ) -> Role: - ... + ) -> Role: ... async def create_role( self, @@ -3468,12 +3466,10 @@ async def edit_welcome_screen( description: str | None = ..., welcome_channels: list[WelcomeScreenChannel] | None = ..., enabled: bool | None = ..., - ) -> WelcomeScreen: - ... + ) -> WelcomeScreen: ... @overload - async def edit_welcome_screen(self) -> None: - ... + async def edit_welcome_screen(self) -> None: ... async def edit_welcome_screen(self, **options): """|coro| diff --git a/discord/member.py b/discord/member.py index 3e60fc1365..494d1faace 100644 --- a/discord/member.py +++ b/discord/member.py @@ -824,9 +824,9 @@ async def edit( await http.edit_my_voice_state(guild_id, voice_state_payload) else: if not suppress: - voice_state_payload[ - "request_to_speak_timestamp" - ] = datetime.datetime.utcnow().isoformat() + voice_state_payload["request_to_speak_timestamp"] = ( + datetime.datetime.utcnow().isoformat() + ) await http.edit_voice_state(guild_id, self.id, voice_state_payload) if voice_channel is not MISSING: @@ -837,9 +837,9 @@ async def edit( if communication_disabled_until is not MISSING: if communication_disabled_until is not None: - payload[ - "communication_disabled_until" - ] = communication_disabled_until.isoformat() + payload["communication_disabled_until"] = ( + communication_disabled_until.isoformat() + ) else: payload["communication_disabled_until"] = communication_disabled_until diff --git a/discord/message.py b/discord/message.py index 333ced7bd7..5f4d3bc994 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1369,8 +1369,7 @@ async def edit( delete_after: float | None = ..., allowed_mentions: AllowedMentions | None = ..., view: View | None = ..., - ) -> Message: - ... + ) -> Message: ... async def edit( self, diff --git a/discord/player.py b/discord/player.py index 0b4dc417c3..b76fe7b366 100644 --- a/discord/player.py +++ b/discord/player.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 diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index f7c0f898d9..cda13f48b9 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -270,11 +270,9 @@ async def edit( name: str = MISSING, description: str = MISSING, status: int | ScheduledEventStatus = MISSING, - location: str - | int - | VoiceChannel - | StageChannel - | ScheduledEventLocation = MISSING, + location: ( + str | int | VoiceChannel | StageChannel | ScheduledEventLocation + ) = MISSING, start_time: datetime.datetime = MISSING, end_time: datetime.datetime = MISSING, cover: bytes | None = MISSING, diff --git a/discord/sinks/__init__.py b/discord/sinks/__init__.py index 7af605af81..6db5209af0 100644 --- a/discord/sinks/__init__.py +++ b/discord/sinks/__init__.py @@ -7,6 +7,7 @@ :copyright: 2021-present Pycord Development :license: MIT, see LICENSE for more details. """ + from .core import * from .errors import * from .m4a import * diff --git a/discord/sinks/core.py b/discord/sinks/core.py index 05d090a07a..20d6d3a9ac 100644 --- a/discord/sinks/core.py +++ b/discord/sinks/core.py @@ -22,6 +22,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import io import os import struct diff --git a/discord/sinks/errors.py b/discord/sinks/errors.py index da1a928566..5f036efff5 100644 --- a/discord/sinks/errors.py +++ b/discord/sinks/errors.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from discord.errors import DiscordException diff --git a/discord/sinks/m4a.py b/discord/sinks/m4a.py index 6b65e3653c..1cff9da538 100644 --- a/discord/sinks/m4a.py +++ b/discord/sinks/m4a.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import io import os import subprocess diff --git a/discord/sinks/mka.py b/discord/sinks/mka.py index 819608d368..c2bbefb923 100644 --- a/discord/sinks/mka.py +++ b/discord/sinks/mka.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import io import subprocess diff --git a/discord/sinks/mkv.py b/discord/sinks/mkv.py index 2071c1ae4f..93f4cc7444 100644 --- a/discord/sinks/mkv.py +++ b/discord/sinks/mkv.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import io import subprocess diff --git a/discord/sinks/mp3.py b/discord/sinks/mp3.py index c4707eb5ed..74386a2738 100644 --- a/discord/sinks/mp3.py +++ b/discord/sinks/mp3.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import io import subprocess diff --git a/discord/sinks/mp4.py b/discord/sinks/mp4.py index 9f540c6c29..c4d0ed2b63 100644 --- a/discord/sinks/mp4.py +++ b/discord/sinks/mp4.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import io import os import subprocess diff --git a/discord/sinks/ogg.py b/discord/sinks/ogg.py index 09abcfc402..7b531464bd 100644 --- a/discord/sinks/ogg.py +++ b/discord/sinks/ogg.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import io import subprocess diff --git a/discord/sinks/pcm.py b/discord/sinks/pcm.py index 5ec8d47634..c587da349a 100644 --- a/discord/sinks/pcm.py +++ b/discord/sinks/pcm.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from .core import Filters, Sink, default_filters diff --git a/discord/sinks/wave.py b/discord/sinks/wave.py index 90645bd602..37f5aac933 100644 --- a/discord/sinks/wave.py +++ b/discord/sinks/wave.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import wave from .core import Filters, Sink, default_filters diff --git a/discord/types/channel.py b/discord/types/channel.py index bc825a3f38..79e7b9ebbf 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.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 from typing import Literal, Union diff --git a/discord/types/embed.py b/discord/types/embed.py index 0d73afe40c..dbfcb14c87 100644 --- a/discord/types/embed.py +++ b/discord/types/embed.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 from typing import Literal diff --git a/discord/types/emoji.py b/discord/types/emoji.py index 68040ebce2..f4d7f64923 100644 --- a/discord/types/emoji.py +++ b/discord/types/emoji.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 from typing import TypedDict diff --git a/discord/types/guild.py b/discord/types/guild.py index 567f36d5cc..be417a726d 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.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 from typing import Literal diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index 44ac45363d..db2f5dde5f 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.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 from .._typed_dict import NotRequired, TypedDict diff --git a/discord/types/user.py b/discord/types/user.py index 6398fe1fc8..a0383e4f04 100644 --- a/discord/types/user.py +++ b/discord/types/user.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 from typing import Literal, TypedDict diff --git a/discord/types/voice.py b/discord/types/voice.py index 353bf1023d..4dc485cbf7 100644 --- a/discord/types/voice.py +++ b/discord/types/voice.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 from typing import Literal diff --git a/discord/types/widget.py b/discord/types/widget.py index daeef2db95..1ebeb959a3 100644 --- a/discord/types/widget.py +++ b/discord/types/widget.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 from .._typed_dict import NotRequired, TypedDict diff --git a/discord/utils.py b/discord/utils.py index 7e41b6d04f..9d982a3aef 100644 --- a/discord/utils.py +++ b/discord/utils.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 array @@ -172,12 +173,12 @@ def __init__(self, name: str, function: Callable[[T], T_co]) -> None: self.__doc__ = getattr(function, "__doc__") @overload - def __get__(self, instance: None, owner: type[T]) -> CachedSlotProperty[T, T_co]: - ... + def __get__( + self, instance: None, owner: type[T] + ) -> CachedSlotProperty[T, T_co]: ... @overload - def __get__(self, instance: T, owner: type[T]) -> T_co: - ... + def __get__(self, instance: T, owner: type[T]) -> T_co: ... def __get__(self, instance: T | None, owner: type[T]) -> Any: if instance is None: @@ -251,18 +252,15 @@ async def inner_call(): @overload -def parse_time(timestamp: None) -> None: - ... +def parse_time(timestamp: None) -> None: ... @overload -def parse_time(timestamp: str) -> datetime.datetime: - ... +def parse_time(timestamp: str) -> datetime.datetime: ... @overload -def parse_time(timestamp: str | None) -> datetime.datetime | None: - ... +def parse_time(timestamp: str | None) -> datetime.datetime | None: ... def parse_time(timestamp: str | None) -> datetime.datetime | None: @@ -791,8 +789,7 @@ class SnowflakeList(array.array): if TYPE_CHECKING: - def __init__(self, data: Iterable[int], *, is_sorted: bool = False): - ... + def __init__(self, data: Iterable[int], *, is_sorted: bool = False): ... def __new__(cls, data: Iterable[int], *, is_sorted: bool = False): return array.array.__new__(cls, "Q", data if is_sorted else sorted(data)) # type: ignore @@ -1093,13 +1090,11 @@ async def _achunk(iterator: AsyncIterator[T], max_size: int) -> AsyncIterator[li @overload -def as_chunks(iterator: Iterator[T], max_size: int) -> Iterator[list[T]]: - ... +def as_chunks(iterator: Iterator[T], max_size: int) -> Iterator[list[T]]: ... @overload -def as_chunks(iterator: AsyncIterator[T], max_size: int) -> AsyncIterator[list[T]]: - ... +def as_chunks(iterator: AsyncIterator[T], max_size: int) -> AsyncIterator[list[T]]: ... def as_chunks(iterator: _Iter[T], max_size: int) -> _Iter[list[T]]: diff --git a/discord/voice_client.py b/discord/voice_client.py index 61bc0ad9d2..46fc293e12 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -630,8 +630,7 @@ def play( *, after: Callable[[Exception | None], Any] | None = None, wait_finish: Literal[False] = False, - ) -> None: - ... + ) -> None: ... @overload def play( @@ -640,8 +639,7 @@ def play( *, after: Callable[[Exception | None], Any] | None = None, wait_finish: Literal[True], - ) -> asyncio.Future: - ... + ) -> asyncio.Future: ... def play( self, diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 09ba8cc327..d14f9e6c5d 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1573,8 +1573,7 @@ async def send( applied_tags: list[Snowflake] = MISSING, wait: Literal[True], delete_after: float = None, - ) -> WebhookMessage: - ... + ) -> WebhookMessage: ... @overload async def send( @@ -1596,8 +1595,7 @@ async def send( applied_tags: list[Snowflake] = MISSING, wait: Literal[False] = ..., delete_after: float = None, - ) -> None: - ... + ) -> None: ... async def send( self, diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 4bcf6b6028..d2d3213d71 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -940,8 +940,7 @@ def send( thread: Snowflake = MISSING, thread_name: str | None = None, wait: Literal[True], - ) -> SyncWebhookMessage: - ... + ) -> SyncWebhookMessage: ... @overload def send( @@ -960,8 +959,7 @@ def send( thread_name: str | None = None, wait: Literal[False] = ..., suppress: bool = MISSING, - ) -> None: - ... + ) -> None: ... def send( self, diff --git a/discord/welcome_screen.py b/discord/welcome_screen.py index ae0a98da08..7e709ab7a6 100644 --- a/discord/welcome_screen.py +++ b/discord/welcome_screen.py @@ -157,12 +157,10 @@ async def edit( welcome_channels: list[WelcomeScreenChannel] | None = ..., enabled: bool | None = ..., reason: str | None = ..., - ) -> None: - ... + ) -> None: ... @overload - async def edit(self) -> None: - ... + async def edit(self) -> None: ... async def edit(self, **options): """|coro| diff --git a/tests/helpers.py b/tests/helpers.py index 843ddf078e..8b62271a5a 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -22,6 +22,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from typing import TypeVar V = TypeVar("V") diff --git a/tests/test_utils.py b/tests/test_utils.py index bf8dc63b99..d0f94acb93 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -22,6 +22,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + # mypy: implicit-reexport=True from typing import TypeVar From 845f7c7d7250887d62bbf65d76468e5d45263773 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:53:58 +0000 Subject: [PATCH 021/108] chore(pre-commit): pre-commit autoupdate (#2361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/autoflake: v2.2.1 → v2.3.0](https://github.com/PyCQA/autoflake/compare/v2.2.1...v2.3.0) - [github.com/asottile/pyupgrade: v3.15.0 → v3.15.1](https://github.com/asottile/pyupgrade/compare/v3.15.0...v3.15.1) - [github.com/psf/black: 24.1.1 → 24.2.0](https://github.com/psf/black/compare/24.1.1...24.2.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b9c3e1b90f..2660004bbf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - repo: https://github.com/PyCQA/autoflake - rev: v2.2.1 + rev: v2.3.0 hooks: - id: autoflake # args: @@ -19,7 +19,7 @@ repos: # - --remove-duplicate-keys # - --remove-unused-variables - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.1 hooks: - id: pyupgrade args: [--py38-plus] @@ -28,7 +28,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 24.1.1 + rev: 24.2.0 hooks: - id: black args: [--safe, --quiet] From d5ed4762a277116536704fc4cf69edee79d4da4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:56:47 -0600 Subject: [PATCH 022/108] chore(deps-dev): update pytest requirement from ~=8.0.0 to ~=8.0.1 (#2358) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.0...8.0.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: plun1331 --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index ebefcfb977..26016ae801 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ -r _.txt pylint~=3.0.3 -pytest~=8.0.0 +pytest~=8.0.1 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 mypy~=1.8.0 From 4f949a89c0504f6683b60f0f1eca58f5176825dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 23:41:14 +0000 Subject: [PATCH 023/108] chore(deps-dev): update pylint requirement from ~=3.0.3 to ~=3.0.4 (#2363) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.0.3...v3.0.4) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 26016ae801..9ee900d721 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r _.txt -pylint~=3.0.3 +pylint~=3.0.4 pytest~=8.0.1 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 From 52d771953e55861727d6913d955083de1cd9a1c6 Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:27:44 +0300 Subject: [PATCH 024/108] fix: type hint kwargs properly (#2364) * fix * chore: changelog --- CHANGELOG.md | 2 ++ discord/ext/bridge/core.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 047ebd159d..48609d7988 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -239,6 +239,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2345](https://github.com/Pycord-Development/pycord/pull/2345)) - Fixed `Thread.me` being out of date and added the thread owner to `Thread.members` on creation. ([#1296](https://github.com/Pycord-Development/pycord/issues/1296)) +- Fixed keyword argument wildcard of `bridge.has_permissions` having the wrong type + hint. ([#2364](https://github.com/Pycord-Development/pycord/pull/2364)) ## [2.4.1] - 2023-03-20 diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py index 4350a1e4e4..b1a37b3980 100644 --- a/discord/ext/bridge/core.py +++ b/discord/ext/bridge/core.py @@ -525,7 +525,7 @@ def predicate(func: Callable | ApplicationCommand): return predicate -def has_permissions(**perms: dict[str, bool]): +def has_permissions(**perms: 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 permissions inside guilds, and also registers the command as locked behind said permissions. From b478e5bf1828ad28141bdc8521dacfe7b7644073 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 01:35:03 +0000 Subject: [PATCH 025/108] chore(deps-dev): update pytest requirement from ~=8.0.1 to ~=8.0.2 (#2365) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.1...8.0.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 9ee900d721..7679d11450 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ -r _.txt pylint~=3.0.4 -pytest~=8.0.1 +pytest~=8.0.2 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 mypy~=1.8.0 From 8984b861b0f5f973703f66258f592d27c6426680 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 01:39:05 +0000 Subject: [PATCH 026/108] chore(deps-dev): update pylint requirement from ~=3.0.4 to ~=3.1.0 (#2366) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.0.4...v3.1.0) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 7679d11450..8b8bf4c6dc 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r _.txt -pylint~=3.0.4 +pylint~=3.1.0 pytest~=8.0.2 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 From 2276914ae5bff93ed13ad1d4b6ff3f1ef50abf81 Mon Sep 17 00:00:00 2001 From: jab416171 Date: Tue, 27 Feb 2024 19:50:30 -0700 Subject: [PATCH 027/108] fix: fixes enum to support stringified annotations (#2367) fixes #2359 --- CHANGELOG.md | 2 ++ discord/enums.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48609d7988..4bfbc36a62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -241,6 +241,8 @@ These changes are available on the `master` branch, but have not yet been releas creation. ([#1296](https://github.com/Pycord-Development/pycord/issues/1296)) - Fixed keyword argument wildcard of `bridge.has_permissions` having the wrong type hint. ([#2364](https://github.com/Pycord-Development/pycord/pull/2364)) +- Fixed enum to support stringified annotations. + ([#2367](https://github.com/Pycord-Development/pycord/pull/2367)) ## [2.4.1] - 2023-03-20 diff --git a/discord/enums.py b/discord/enums.py index d954d5d56c..f2c7c2e60f 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -800,9 +800,13 @@ def from_datatype(cls, datatype): # Type checking fails for this case, so ignore it. return cls.from_datatype(datatype.__args__) # type: ignore - if datatype.__name__ in ["Member", "User"]: + if isinstance(datatype, str): + datatype_name = datatype + else: + datatype_name = datatype.__name__ + if datatype_name in ["Member", "User"]: return cls.user - if datatype.__name__ in [ + if datatype_name in [ "GuildChannel", "TextChannel", "VoiceChannel", @@ -814,14 +818,14 @@ def from_datatype(cls, datatype): "DMChannel", ]: return cls.channel - if datatype.__name__ == "Role": + if datatype_name == "Role": return cls.role - if datatype.__name__ == "Attachment": + if datatype_name == "Attachment": return cls.attachment - if datatype.__name__ == "Mentionable": + if datatype_name == "Mentionable": return cls.mentionable - if issubclass(datatype, str): + if isinstance(datatype, str) or issubclass(datatype, str): return cls.string if issubclass(datatype, bool): return cls.boolean From 76a3e4c24f95aa6f71429f7fbc22a7298c617cc7 Mon Sep 17 00:00:00 2001 From: Icebluewolf <44532864+Icebluewolf@users.noreply.github.com> Date: Wed, 28 Feb 2024 09:40:17 -0600 Subject: [PATCH 028/108] feat: implement voice channel statuses (#2368) Signed-off-by: Icebluewolf <44532864+Icebluewolf@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 Co-authored-by: Lala Sabathil Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/channel.py | 42 +++++++++++++++++++++++++++++++++++++ discord/http.py | 7 +++++++ discord/permissions.py | 13 ++++++++++-- discord/raw_models.py | 32 ++++++++++++++++++++++++++++ discord/state.py | 24 +++++++++++++++++++++ discord/types/channel.py | 1 + discord/types/raw_models.py | 6 ++++++ docs/api/events.rst | 25 ++++++++++++++++++++++ docs/api/models.rst | 5 +++++ 10 files changed, 155 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bfbc36a62..d0923c07b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2342](https://github.com/Pycord-Development/pycord/pull/2342)) - Added `invitable` and `slowmode_delay` to `Thread` creation methods. ([#2350](https://github.com/Pycord-Development/pycord/pull/2350)) +- Added support for voice channel statuses. + ([#2368](https://github.com/Pycord-Development/pycord/pull/2368)) ### Changed diff --git a/discord/channel.py b/discord/channel.py index afa6780515..2586e3eed7 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1519,6 +1519,10 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): Bots and users with :attr:`~Permissions.manage_channels` or :attr:`~Permissions.manage_messages` bypass slowmode. + .. versionadded:: 2.5 + status: Optional[:class:`str`] + The channel's status, if set. + .. versionadded:: 2.5 flags: :class:`ChannelFlags` Extra features of the channel. @@ -1526,13 +1530,26 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): .. versionadded:: 2.0 """ + def __init__( + self, + *, + state: ConnectionState, + guild: Guild, + data: VoiceChannelPayload, + ): + self.status: str | None = None + super().__init__(state=state, guild=guild, data=data) + def _update(self, guild: Guild, data: VoiceChannelPayload): super()._update(guild, data) + if data.get("status"): + self.status = data.get("status") def __repr__(self) -> str: attrs = [ ("id", self.id), ("name", self.name), + ("status", self.status), ("rtc_region", self.rtc_region), ("position", self.position), ("bitrate", self.bitrate), @@ -1955,6 +1972,31 @@ async def create_activity_invite( **kwargs, ) + async def set_status( + self, status: str | None, *, reason: str | None = None + ) -> None: + """|coro| + + Sets the status of the voice channel. + + You must have the :attr:`~Permissions.set_voice_channel_status` permission to use this. + + Parameters + ---------- + status: Union[:class:`str`, None] + The new status. + reason: Optional[:class:`str`] + The reason for setting the status. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have proper permissions to set the status. + HTTPException + Setting the status failed. + """ + await self._state.http.set_voice_channel_status(self.id, status, reason=reason) + class StageChannel(discord.abc.Messageable, VocalGuildChannel): """Represents a Discord guild stage channel. diff --git a/discord/http.py b/discord/http.py index b5eadc031d..acf0807a3b 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2185,6 +2185,13 @@ def move_member( guild_id=guild_id, user_id=user_id, channel_id=channel_id, reason=reason ) + def set_voice_channel_status( + self, channel_id: Snowflake, status: str | None, *, reason: str | None = None + ) -> Response[None]: + payload = {"status": status} + r = Route("PUT", "/channels/{channel_id}/voice-status", channel_id=channel_id) + return self.request(r, json=payload, reason=reason) + # Stage instance management def get_stage_instance( diff --git a/discord/permissions.py b/discord/permissions.py index e94e6c2116..75b71d57cd 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -180,7 +180,7 @@ def all(cls: type[P]) -> P: """A factory method that creates a :class:`Permissions` with all permissions set to ``True``. """ - return cls(0b11111111111111111111111111111111111111111) + return cls(0b1111111111111111111111111111111111111111111111111) @classmethod def all_channel(cls: type[P]) -> P: @@ -250,7 +250,7 @@ def voice(cls: type[P]) -> P: """A factory method that creates a :class:`Permissions` with all "Voice" permissions from the official Discord UI set to ``True``. """ - return cls(0b00000011111100000000001100000000) + return cls(0b1000000001000000000000011111100000000001100000000) @classmethod def stage(cls: type[P]) -> P: @@ -618,6 +618,14 @@ def send_voice_messages(self) -> int: """ return 1 << 46 + @flag_value + def set_voice_channel_status(self) -> int: + """:class:`bool`: Returns ``True`` if a member can set voice channel status. + + .. versionadded:: 2.5 + """ + return 1 << 48 + PO = TypeVar("PO", bound="PermissionOverwrite") @@ -736,6 +744,7 @@ class PermissionOverwrite: start_embedded_activities: bool | None moderate_members: bool | None send_voice_messages: bool | None + set_voice_channel_status: bool | None def __init__(self, **kwargs: bool | None): self._values: dict[str, bool | None] = {} diff --git a/discord/raw_models.py b/discord/raw_models.py index 79c8091d98..873c1fe0bd 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -56,6 +56,7 @@ ThreadMembersUpdateEvent, ThreadUpdateEvent, TypingEvent, + VoiceChannelStatusUpdateEvent, ) @@ -75,6 +76,7 @@ "AutoModActionExecutionEvent", "RawThreadMembersUpdateEvent", "RawAuditLogEntryEvent", + "RawVoiceChannelStatusUpdateEvent", ) @@ -441,6 +443,36 @@ def __init__(self, data: ThreadDeleteEvent) -> None: self.data: ThreadDeleteEvent = data +class RawVoiceChannelStatusUpdateEvent(_RawReprMixin): + """Represents the payload for an :func:`on_raw_voice_channel_status_update` event. + + .. versionadded:: 2.5 + + Attributes + ---------- + id: :class:`int` + The channel ID where the voice channel status update originated from. + guild_id: :class:`int` + The guild ID where the voice channel status update originated from. + status: Optional[:class:`str`] + The new new voice channel status. + data: :class:`dict` + The raw data sent by the `gateway `_. + """ + + __slots__ = ("id", "guild_id", "status", "data") + + def __init__(self, data: VoiceChannelStatusUpdateEvent) -> None: + self.id: int = int(data["id"]) + self.guild_id: int = int(data["guild_id"]) + + try: + self.status: str | None = data["status"] + except KeyError: + self.status: str | None = None + self.data: VoiceChannelStatusUpdateEvent = data + + class RawTypingEvent(_RawReprMixin): """Represents the payload for a :func:`on_raw_typing` event. diff --git a/discord/state.py b/discord/state.py index 011dd0e142..5b9aea0e35 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1785,6 +1785,30 @@ def parse_voice_server_update(self, data) -> None: ) ) + def parse_voice_channel_status_update(self, data) -> None: + raw = RawVoiceChannelStatusUpdateEvent(data) + self.dispatch("raw_voice_channel_status_update", raw) + guild = self._get_guild(int(data["guild_id"])) + channel_id = int(data["id"]) + if guild is not None: + channel = guild.get_channel(channel_id) + if channel is not None: + old_status = channel.status + channel.status = data.get("status", None) + self.dispatch( + "voice_channel_status_update", channel, old_status, channel.status + ) + else: + _log.debug( + "VOICE_CHANNEL_STATUS_UPDATE referencing an unknown channel ID: %s. Discarding.", + channel_id, + ) + else: + _log.debug( + "VOICE_CHANNEL_STATUS_UPDATE referencing unknown guild ID: %s. Discarding.", + data["guild_id"], + ) + def parse_typing_start(self, data) -> None: raw = RawTypingEvent(data) diff --git a/discord/types/channel.py b/discord/types/channel.py index 79e7b9ebbf..0f4c044aed 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -108,6 +108,7 @@ class NewsChannel(_BaseGuildChannel, _TextChannelOptional): class VoiceChannel(_BaseGuildChannel): rtc_region: NotRequired[str | None] video_quality_mode: NotRequired[VideoQualityMode] + status: NotRequired[str | None] type: Literal[2] bitrate: int user_limit: int diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index db2f5dde5f..de2b3fbf2b 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.py @@ -131,6 +131,12 @@ class MemberRemoveEvent(TypedDict): user: User +class VoiceChannelStatusUpdateEvent(TypedDict): + id: Snowflake + guild_id: Snowflake + status: NotRequired[str] + + class ThreadMembersUpdateEvent(TypedDict): id: Snowflake guild_id: Snowflake diff --git a/docs/api/events.rst b/docs/api/events.rst index 8a0b76d13b..25edc139b0 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -1299,3 +1299,28 @@ Typing :param payload: The raw typing payload. :type payload: :class:`RawTypingEvent` + + +Voice Channel Status Update +--------------------------- +.. function:: on_voice_channel_status_update(channel, before, after) + + Called when someone updates a voice channel status. + + .. versionadded:: 2.5 + + :param channel: The channel where the voice channel status update originated from. + :type channel: :class:`abc.GuildChannel` + :param before: The old voice channel status. + :type before: Optional[:class:`str`] + :param after: The new voice channel status. + :type after: Optional[:class:`str`] + +.. function:: on_raw_voice_channel_status_update(payload) + + Called when someone updates a voice channels status. + + .. versionadded:: 2.5 + + :param payload: The raw voice channel status update payload. + :type payload: :class:`RawVoiceChannelStatusUpdateEvent` diff --git a/docs/api/models.rst b/docs/api/models.rst index 4fce609baf..5fec2f0dd0 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -556,6 +556,11 @@ Events .. autoclass:: RawAuditLogEntryEvent() :members: +.. attributetable:: RawVoiceChannelStatusUpdateEvent + +.. autoclass:: RawVoiceChannelStatusUpdateEvent() + :members: + Webhooks From 752a38c174da5e2021c04a578545e4dd68228852 Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Wed, 28 Feb 2024 20:24:00 +0300 Subject: [PATCH 029/108] feat: Message.enforce_nonce (#2370) Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/abc.py | 12 ++++++++++++ discord/http.py | 9 +++++++++ discord/message.py | 9 +++++++++ discord/types/message.py | 1 + 5 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0923c07b0..3b6110790d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2350](https://github.com/Pycord-Development/pycord/pull/2350)) - Added support for voice channel statuses. ([#2368](https://github.com/Pycord-Development/pycord/pull/2368)) +- Added `Message.enforce_nonce`. + ([#2370](https://github.com/Pycord-Development/pycord/pull/2370)) ### Changed diff --git a/discord/abc.py b/discord/abc.py index d28a87edb9..5c3f362745 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1346,6 +1346,7 @@ async def send( stickers: Sequence[GuildSticker | StickerItem] = ..., delete_after: float = ..., nonce: str | int = ..., + enforce_nonce: bool = ..., allowed_mentions: AllowedMentions = ..., reference: Message | MessageReference | PartialMessage = ..., mention_author: bool = ..., @@ -1365,6 +1366,7 @@ async def send( stickers: Sequence[GuildSticker | StickerItem] = ..., delete_after: float = ..., nonce: str | int = ..., + enforce_nonce: bool = ..., allowed_mentions: AllowedMentions = ..., reference: Message | MessageReference | PartialMessage = ..., mention_author: bool = ..., @@ -1384,6 +1386,7 @@ async def send( stickers: Sequence[GuildSticker | StickerItem] = ..., delete_after: float = ..., nonce: str | int = ..., + enforce_nonce: bool = ..., allowed_mentions: AllowedMentions = ..., reference: Message | MessageReference | PartialMessage = ..., mention_author: bool = ..., @@ -1403,6 +1406,7 @@ async def send( stickers: Sequence[GuildSticker | StickerItem] = ..., delete_after: float = ..., nonce: str | int = ..., + enforce_nonce: bool = ..., allowed_mentions: AllowedMentions = ..., reference: Message | MessageReference | PartialMessage = ..., mention_author: bool = ..., @@ -1423,6 +1427,7 @@ async def send( stickers=None, delete_after=None, nonce=None, + enforce_nonce=None, allowed_mentions=None, reference=None, mention_author=None, @@ -1463,6 +1468,10 @@ async def send( nonce: :class:`int` The nonce to use for sending this message. If the message was successfully sent, then the message will have a nonce with this value. + enforce_nonce: Optional[:class:`bool`] + Whether :attr:`nonce` is enforced to be validated. + + .. versionadded:: 2.5 delete_after: :class:`float` If provided, the number of seconds to wait in the background before deleting the message we just sent. If the deletion fails, @@ -1602,6 +1611,7 @@ async def send( embed=embed, embeds=embeds, nonce=nonce, + enforce_nonce=nonce, message_reference=reference, stickers=stickers, components=components, @@ -1627,6 +1637,7 @@ async def send( embed=embed, embeds=embeds, nonce=nonce, + enforce_nonce=nonce, allowed_mentions=allowed_mentions, message_reference=reference, stickers=stickers, @@ -1644,6 +1655,7 @@ async def send( embed=embed, embeds=embeds, nonce=nonce, + enforce_nonce=nonce, allowed_mentions=allowed_mentions, message_reference=reference, stickers=stickers, diff --git a/discord/http.py b/discord/http.py index acf0807a3b..ecbebfdb87 100644 --- a/discord/http.py +++ b/discord/http.py @@ -465,6 +465,7 @@ def send_message( embed: embed.Embed | None = None, embeds: list[embed.Embed] | None = None, nonce: str | None = None, + enforce_nonce: bool | None = None, allowed_mentions: message.AllowedMentions | None = None, message_reference: message.MessageReference | None = None, stickers: list[sticker.StickerItem] | None = None, @@ -489,6 +490,9 @@ def send_message( if nonce: payload["nonce"] = nonce + if enforce_nonce: + payload["enforce_nonce"] = enforce_nonce + if allowed_mentions: payload["allowed_mentions"] = allowed_mentions @@ -521,6 +525,7 @@ def send_multipart_helper( embed: embed.Embed | None = None, embeds: Iterable[embed.Embed | None] | None = None, nonce: str | None = None, + enforce_nonce: bool | None = None, allowed_mentions: message.AllowedMentions | None = None, message_reference: message.MessageReference | None = None, stickers: list[sticker.StickerItem] | None = None, @@ -538,6 +543,8 @@ def send_multipart_helper( payload["embeds"] = embeds if nonce: payload["nonce"] = nonce + if enforce_nonce: + payload["enforce_nonce"] = enforce_nonce if allowed_mentions: payload["allowed_mentions"] = allowed_mentions if message_reference: @@ -581,6 +588,7 @@ def send_files( embed: embed.Embed | None = None, embeds: list[embed.Embed] | None = None, nonce: str | None = None, + enforce_nonce: bool | None = None, allowed_mentions: message.AllowedMentions | None = None, message_reference: message.MessageReference | None = None, stickers: list[sticker.StickerItem] | None = None, @@ -596,6 +604,7 @@ def send_files( embed=embed, embeds=embeds, nonce=nonce, + enforce_nonce=enforce_nonce, allowed_mentions=allowed_mentions, message_reference=message_reference, stickers=stickers, diff --git a/discord/message.py b/discord/message.py index 5f4d3bc994..453b39f065 100644 --- a/discord/message.py +++ b/discord/message.py @@ -640,6 +640,10 @@ class Message(Hashable): nonce: Optional[Union[:class:`str`, :class:`int`]] The value used by the discord guild and the client to verify that the message is successfully sent. This is not stored long term within Discord's servers and is only used ephemerally. + enforce_nonce: Optional[:class:`bool`] + Whether :attr:`nonce` is enforced to be validated. + + .. versionadded:: 2.5 embeds: List[:class:`Embed`] A list of embeds the message has. channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`, :class:`PartialMessageable`] @@ -748,6 +752,7 @@ class Message(Hashable): "author", "attachments", "nonce", + "enforce_nonce", "pinned", "role_mentions", "type", @@ -802,6 +807,7 @@ def __init__( self.tts: bool = data["tts"] self.content: str = data["content"] self.nonce: int | str | None = data.get("nonce") + self.enforce_nonce: bool | None = data.get("enforce_nonce") self.stickers: list[StickerItem] = [ StickerItem(data=d, state=state) for d in data.get("sticker_items", []) ] @@ -983,6 +989,9 @@ def _handle_embeds(self, value: list[EmbedPayload]) -> None: def _handle_nonce(self, value: str | int) -> None: self.nonce = value + def _handle_enforce_none(self, value: bool) -> None: + self.enforce_nonce = value + def _handle_author(self, author: UserPayload) -> None: self.author = self._state.store_user(author) if isinstance(self.guild, Guild): diff --git a/discord/types/message.py b/discord/types/message.py index 10d819ebd4..a36e43b696 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -111,6 +111,7 @@ class Message(TypedDict): mention_channels: NotRequired[list[ChannelMention]] reactions: NotRequired[list[Reaction]] nonce: NotRequired[int | str] + enforce_nonce: NotRequired[bool] webhook_id: NotRequired[Snowflake] activity: NotRequired[MessageActivity] application: NotRequired[MessageApplication] From be87ae7e497eb3c8de1bf96a651b572081d47414 Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Sat, 2 Mar 2024 08:12:50 +0300 Subject: [PATCH 030/108] feat: audit logs for voice channel status (#2373) Co-authored-by: Lala Sabathil Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/enums.py | 6 ++++++ discord/types/audit_log.py | 3 +++ 3 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b6110790d..423098cfa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2368](https://github.com/Pycord-Development/pycord/pull/2368)) - Added `Message.enforce_nonce`. ([#2370](https://github.com/Pycord-Development/pycord/pull/2370)) +- Added audit log support for voice channel status. + ([#2373](https://github.com/Pycord-Development/pycord/pull/2373)) ### Changed diff --git a/discord/enums.py b/discord/enums.py index f2c7c2e60f..98e46e646d 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -439,6 +439,8 @@ class AuditLogAction(Enum): onboarding_update = 167 server_guide_create = 190 server_guide_update = 191 + voice_channel_status_update = 192 + voice_channel_status_delete = 193 @property def category(self) -> AuditLogActionCategory | None: @@ -506,6 +508,8 @@ def category(self) -> AuditLogActionCategory | None: AuditLogAction.onboarding_update: AuditLogActionCategory.update, AuditLogAction.server_guide_create: AuditLogActionCategory.create, AuditLogAction.server_guide_update: AuditLogActionCategory.update, + AuditLogAction.voice_channel_status_update: AuditLogActionCategory.update, + AuditLogAction.voice_channel_status_delete: AuditLogActionCategory.delete, } return lookup[self] @@ -552,6 +556,8 @@ def target_type(self) -> str | None: return "onboarding" elif v < 192: return "server_guide" + elif v < 194: + return "voice_channel_status" class UserFlags(Enum): diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py index c0b1303947..d543f9e3b7 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -112,6 +112,7 @@ class _AuditLogChange_Str(TypedDict): "deny", "permissions", "tags", + "status", ] new_value: str old_value: str @@ -270,6 +271,8 @@ class AuditEntryInfo(TypedDict): application_id: Snowflake auto_moderation_rule_name: str auto_moderation_rule_trigger_type: str + # TODO: add integration_type + status: str class AuditLogEntry(TypedDict): From d06b72cf59f558617ff0797c13e92826fa5c85a6 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sat, 2 Mar 2024 06:29:59 +0100 Subject: [PATCH 031/108] add missing docs for voice channel status audit log (#2374) Signed-off-by: Lala Sabathil --- docs/api/audit_logs.rst | 8 ++++++++ docs/api/enums.rst | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/api/audit_logs.rst b/docs/api/audit_logs.rst index f974e9602b..08c27ee640 100644 --- a/docs/api/audit_logs.rst +++ b/docs/api/audit_logs.rst @@ -516,5 +516,13 @@ this goal, it must make use of a couple of data classes that aid in this goal. :type: :class:`int` + .. attribute:: status + + The voice channel status of a :class:`VoiceChannel`. + + See also :attr:`VoiceChannel.status`. + + :type: :class:`str` + .. this is currently missing the following keys: reason and application_id I'm not sure how to about porting these diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 431934cc65..0500e89318 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -1592,6 +1592,34 @@ of :class:`enum.Enum`. .. versionadded:: 2.5 + .. attribute:: voice_channel_status_update + + A voice channel status was updated. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`VoiceChannel` or :class:`Object` with the ID of the voice + channel which was updated. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.status` + + .. versionadded:: 2.5 + + .. attribute:: voice_channel_status_delete + + A voice channel status was deleted. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`VoiceChannel` or :class:`Object` with the ID of the voice + channel which was updated. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.status` + + .. versionadded:: 2.5 + .. class:: AuditLogActionCategory From a86145fd92a1b84f82c20f7ee5d4141151a6fe9c Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sat, 2 Mar 2024 09:07:23 +0100 Subject: [PATCH 032/108] fix: correct `enforce_nonce` implementation (#2376) * fix: enforce_nonce mistake * Update discord/message.py Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * fix * Update CHANGELOG.md Signed-off-by: Lala Sabathil --------- Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: Lala Sabathil Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 2 +- discord/abc.py | 6 +++--- discord/message.py | 9 --------- discord/types/message.py | 1 - 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 423098cfa1..33f0ae6b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,7 +95,7 @@ These changes are available on the `master` branch, but have not yet been releas ([#2350](https://github.com/Pycord-Development/pycord/pull/2350)) - Added support for voice channel statuses. ([#2368](https://github.com/Pycord-Development/pycord/pull/2368)) -- Added `Message.enforce_nonce`. +- Added `enforce_nonce` parameter for message sending. ([#2370](https://github.com/Pycord-Development/pycord/pull/2370)) - Added audit log support for voice channel status. ([#2373](https://github.com/Pycord-Development/pycord/pull/2373)) diff --git a/discord/abc.py b/discord/abc.py index 5c3f362745..dc2237151d 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1611,7 +1611,7 @@ async def send( embed=embed, embeds=embeds, nonce=nonce, - enforce_nonce=nonce, + enforce_nonce=enforce_nonce, message_reference=reference, stickers=stickers, components=components, @@ -1637,7 +1637,7 @@ async def send( embed=embed, embeds=embeds, nonce=nonce, - enforce_nonce=nonce, + enforce_nonce=enforce_nonce, allowed_mentions=allowed_mentions, message_reference=reference, stickers=stickers, @@ -1655,7 +1655,7 @@ async def send( embed=embed, embeds=embeds, nonce=nonce, - enforce_nonce=nonce, + enforce_nonce=enforce_nonce, allowed_mentions=allowed_mentions, message_reference=reference, stickers=stickers, diff --git a/discord/message.py b/discord/message.py index 453b39f065..5f4d3bc994 100644 --- a/discord/message.py +++ b/discord/message.py @@ -640,10 +640,6 @@ class Message(Hashable): nonce: Optional[Union[:class:`str`, :class:`int`]] The value used by the discord guild and the client to verify that the message is successfully sent. This is not stored long term within Discord's servers and is only used ephemerally. - enforce_nonce: Optional[:class:`bool`] - Whether :attr:`nonce` is enforced to be validated. - - .. versionadded:: 2.5 embeds: List[:class:`Embed`] A list of embeds the message has. channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`, :class:`PartialMessageable`] @@ -752,7 +748,6 @@ class Message(Hashable): "author", "attachments", "nonce", - "enforce_nonce", "pinned", "role_mentions", "type", @@ -807,7 +802,6 @@ def __init__( self.tts: bool = data["tts"] self.content: str = data["content"] self.nonce: int | str | None = data.get("nonce") - self.enforce_nonce: bool | None = data.get("enforce_nonce") self.stickers: list[StickerItem] = [ StickerItem(data=d, state=state) for d in data.get("sticker_items", []) ] @@ -989,9 +983,6 @@ def _handle_embeds(self, value: list[EmbedPayload]) -> None: def _handle_nonce(self, value: str | int) -> None: self.nonce = value - def _handle_enforce_none(self, value: bool) -> None: - self.enforce_nonce = value - def _handle_author(self, author: UserPayload) -> None: self.author = self._state.store_user(author) if isinstance(self.guild, Guild): diff --git a/discord/types/message.py b/discord/types/message.py index a36e43b696..10d819ebd4 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -111,7 +111,6 @@ class Message(TypedDict): mention_channels: NotRequired[list[ChannelMention]] reactions: NotRequired[list[Reaction]] nonce: NotRequired[int | str] - enforce_nonce: NotRequired[bool] webhook_id: NotRequired[Snowflake] activity: NotRequired[MessageActivity] application: NotRequired[MessageApplication] From 35bb80e2ab5dfe8854e4bf6e8431af1ce71f0317 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sat, 2 Mar 2024 09:13:13 +0100 Subject: [PATCH 033/108] chore: bump changelog version (#2375) * chore: bump changelog version * Update CHANGELOG.md Signed-off-by: Lala Sabathil * Update CHANGELOG.md Signed-off-by: Lala Sabathil * Update CHANGELOG.md Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * sorry Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * Update CHANGELOG.md Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --------- Signed-off-by: Lala Sabathil Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f0ae6b09..7b9b3eae4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ possible (see our [Version Guarantees] for more info). These changes are available on the `master` branch, but have not yet been released. +## [2.5.0] - 2024-03-02 + ### Added - Added method to start bot via async context manager. @@ -778,7 +780,8 @@ These changes are available on the `master` branch, but have not yet been releas - Fix py3.10 UnionType checks issue. ([#1240](https://github.com/Pycord-Development/pycord/pull/1240)) -[unreleased]: https://github.com/Pycord-Development/pycord/compare/v2.4.1...HEAD +[unreleased]: https://github.com/Pycord-Development/pycord/compare/v2.5.0...HEAD +[2.5.0]: https://github.com/Pycord-Development/pycord/compare/v2.4.1...v2.5.0 [2.4.1]: https://github.com/Pycord-Development/pycord/compare/v2.4.0...v2.4.1 [2.4.0]: https://github.com/Pycord-Development/pycord/compare/v2.3.3...v2.4.0 [2.3.3]: https://github.com/Pycord-Development/pycord/compare/v2.3.2...v2.3.3 From 87d2331b1e0896621acf2c77ec14e7fcf39b0671 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sat, 2 Mar 2024 09:54:48 +0100 Subject: [PATCH 034/108] chore(deps): use furo from pypi (#2377) fix: use furo from pypi --- requirements/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/docs.txt b/requirements/docs.txt index 605916439c..d309bbdf31 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -4,5 +4,5 @@ sphinxcontrib-websupport==1.2.4 myst-parser==1.0.0 sphinxext-opengraph==0.9.1 sphinx-copybutton==0.5.2 -furo@ git+https://github.com/pradyunsg/furo@193643f +furo==2023.3.23 sphinx-autodoc-typehints==1.23.0 From 9b1c86ce8e340f28e594983fbcddceefa2c97482 Mon Sep 17 00:00:00 2001 From: YoggieS <61660055+yoggys@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:18:54 +0100 Subject: [PATCH 035/108] feat: Change type of `Guild.bitrate_limit` to `int`. (#2387) * feat: Change type of `Guild.bitrate_limit` to `int`. * style(pre-commit): auto fixes from pre-commit.com hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ discord/guild.py | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b9b3eae4b..568fdf8b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ possible (see our [Version Guarantees] for more info). These changes are available on the `master` branch, but have not yet been released. +### Changed + +- Changed the type of `Guild.bitrate_limit` to `int`. + ([#2387](https://github.com/Pycord-Development/pycord/pull/2387)) + ## [2.5.0] - 2024-03-02 ### Added diff --git a/discord/guild.py b/discord/guild.py index bdd3bc1f20..c29b0f7157 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -823,14 +823,16 @@ def sticker_limit(self) -> int: ) @property - def bitrate_limit(self) -> float: + def bitrate_limit(self) -> int: """The maximum bitrate for voice channels this guild can have.""" vip_guild = ( self._PREMIUM_GUILD_LIMITS[1].bitrate if "VIP_REGIONS" in self.features else 96e3 ) - return max(vip_guild, self._PREMIUM_GUILD_LIMITS[self.premium_tier].bitrate) + return int( + max(vip_guild, self._PREMIUM_GUILD_LIMITS[self.premium_tier].bitrate) + ) @property def filesize_limit(self) -> int: From d6693d3b50edadf0ce8500e906c18a65a867150b Mon Sep 17 00:00:00 2001 From: YoggieS <61660055+yoggys@users.noreply.github.com> Date: Fri, 8 Mar 2024 21:50:59 +0100 Subject: [PATCH 036/108] fix: Proper type-hinting & docstring of `Member.move_to` and `Member.edit` (#2386) * fix: proper type-hinting of `Member.move_to` * style(pre-commit): auto fixes from pre-commit.com hooks * Update CHANGELOG.md Signed-off-by: Lala Sabathil * add pr Signed-off-by: Lala Sabathil * Update CHANGELOG.md Signed-off-by: plun1331 * fix: docstring for `Member.edit` and `Member.move_to` * style(pre-commit): auto fixes from pre-commit.com hooks * further docstrings updates & typing --------- Signed-off-by: Lala Sabathil Signed-off-by: plun1331 Signed-off-by: YoggieS <61660055+yoggys@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil Co-authored-by: plun1331 --- CHANGELOG.md | 5 +++++ discord/flags.py | 5 +++++ discord/guild.py | 4 ++-- discord/member.py | 6 +++--- discord/raw_models.py | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568fdf8b70..a8b74b924c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ possible (see our [Version Guarantees] for more info). These changes are available on the `master` branch, but have not yet been released. +### Fixed + +- Fixed the type-hinting of `Member.move_to` and `Member.edit` to reflect actual + behavior. ([#2386](https://github.com/Pycord-Development/pycord/pull/2386)) + ### Changed - Changed the type of `Guild.bitrate_limit` to `int`. diff --git a/discord/flags.py b/discord/flags.py index 7cc6233c33..3201e5be89 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -830,6 +830,11 @@ def voice_states(self): - :attr:`VoiceChannel.members` - :attr:`VoiceChannel.voice_states` + - :attr:`StageChannel.members` + - :attr:`StageChannel.speakers` + - :attr:`StageChannel.listeners` + - :attr:`StageChannel.moderators` + - :attr:`StageChannel.voice_states` - :attr:`Member.voice` .. note:: diff --git a/discord/guild.py b/discord/guild.py index c29b0f7157..d1044cf3f8 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -539,7 +539,7 @@ def _from_data(self, guild: GuildPayload) -> None: ) self.owner_id: int | None = utils._get_as_snowflake(guild, "owner_id") - self.afk_channel: VocalGuildChannel | None = self.get_channel( + self.afk_channel: VoiceChannel | None = self.get_channel( utils._get_as_snowflake(guild, "afk_channel_id") ) # type: ignore @@ -3422,7 +3422,7 @@ async def change_voice_state( Parameters ---------- - channel: Optional[:class:`VoiceChannel`] + channel: Optional[Union[:class:`VoiceChannel`, :class:`StageChannel`]] Channel the client wants to join. Use ``None`` to disconnect. self_mute: :class:`bool` Indicates if the client should be self-muted. diff --git a/discord/member.py b/discord/member.py index 494d1faace..fecb9b9dfa 100644 --- a/discord/member.py +++ b/discord/member.py @@ -769,7 +769,7 @@ async def edit( roles: List[:class:`Role`] The member's new list of roles. This *replaces* the roles. - voice_channel: Optional[:class:`VoiceChannel`] + voice_channel: Optional[Union[:class:`VoiceChannel`, :class:`StageChannel`]] The voice channel to move the member to. Pass ``None`` to kick them from voice. reason: Optional[:class:`str`] @@ -958,7 +958,7 @@ async def request_to_speak(self) -> None: await self._state.http.edit_my_voice_state(self.guild.id, payload) async def move_to( - self, channel: VocalGuildChannel, *, reason: str | None = None + self, channel: VocalGuildChannel | None, *, reason: str | None = None ) -> None: """|coro| @@ -974,7 +974,7 @@ async def move_to( Parameters ---------- - channel: Optional[:class:`VoiceChannel`] + channel: Optional[Union[:class:`VoiceChannel`, :class:`StageChannel`]] The new voice channel to move the member to. Pass ``None`` to kick them from voice. reason: Optional[:class:`str`] diff --git a/discord/raw_models.py b/discord/raw_models.py index 873c1fe0bd..49f5575064 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -596,7 +596,7 @@ class AutoModActionExecutionEvent: The member that triggered the action, if cached. channel_id: Optional[:class:`int`] The ID of the channel in which the member's content was posted. - channel: Optional[Union[:class:`TextChannel`, :class:`Thread`, :class:`VoiceChannel`]] + channel: Optional[Union[:class:`TextChannel`, :class:`Thread`, :class:`VoiceChannel`, :class:`StageChannel`]] The channel in which the member's content was posted, if cached. message_id: Optional[:class:`int`] The ID of the message that triggered the action. This is only available if the From d4d3d20bd7689be1295cd1feb925dcdc5919bf58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 Mar 2024 18:14:46 +0000 Subject: [PATCH 037/108] chore(deps-dev): bump bandit from 1.7.7 to 1.7.8 (#2388) Bumps [bandit](https://github.com/PyCQA/bandit) from 1.7.7 to 1.7.8. - [Release notes](https://github.com/PyCQA/bandit/releases) - [Commits](https://github.com/PyCQA/bandit/compare/1.7.7...1.7.8) --- updated-dependencies: - dependency-name: bandit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 8b8bf4c6dc..9d69c2e787 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -7,5 +7,5 @@ mypy~=1.8.0 coverage~=7.4 pre-commit==3.5.0 codespell==2.2.6 -bandit==1.7.7 +bandit==1.7.8 flake8==7.0.0 From 7c0689247309aaef38d36b229f9c1fbea56edf61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 Mar 2024 18:17:58 +0000 Subject: [PATCH 038/108] chore(deps-dev): update mypy requirement from ~=1.8.0 to ~=1.9.0 (#2389) Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.8.0...1.9.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: plun1331 --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 9d69c2e787..2822060957 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -3,7 +3,7 @@ pylint~=3.1.0 pytest~=8.0.2 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 -mypy~=1.8.0 +mypy~=1.9.0 coverage~=7.4 pre-commit==3.5.0 codespell==2.2.6 From 0c68e5f592e356f8251faff0ec8dd9f8cacfea2e Mon Sep 17 00:00:00 2001 From: Revnoplex <62947003+Revnoplex@users.noreply.github.com> Date: Sun, 17 Mar 2024 12:56:29 +1100 Subject: [PATCH 039/108] fix: Replace deprecated pkg_resources with importlib.metadata to clear DeprecationWarning (#2392) Signed-off-by: Lala Sabathil Signed-off-by: Revnoplex <62947003+Revnoplex@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> --- CHANGELOG.md | 3 +++ discord/__main__.py | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b74b924c..ac49862f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ These changes are available on the `master` branch, but have not yet been releas - Fixed the type-hinting of `Member.move_to` and `Member.edit` to reflect actual behavior. ([#2386](https://github.com/Pycord-Development/pycord/pull/2386)) +- Fixed a deprecation warning from being displayed when running `python -m discord -v` + by replacing the deprecated module. + ([#2392](https://github.com/Pycord-Development/pycord/pull/2392)) ### Changed diff --git a/discord/__main__.py b/discord/__main__.py index 12b5b5a1c3..ed34bdf42a 100644 --- a/discord/__main__.py +++ b/discord/__main__.py @@ -24,13 +24,13 @@ """ import argparse +import importlib.metadata import platform import sys from pathlib import Path from typing import Tuple import aiohttp -import pkg_resources import discord @@ -47,9 +47,9 @@ def show_version() -> None: "- py-cord v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}".format(version_info) ) if version_info.releaselevel != "final": - pkg = pkg_resources.get_distribution("py-cord") - if pkg: - entries.append(f" - py-cord pkg_resources: v{pkg.version}") + version = importlib.metadata.version("py-cord") + if version: + entries.append(f" - py-cord importlib.metadata: v{version}") entries.append(f"- aiohttp v{aiohttp.__version__}") uname = platform.uname() From 530f3d45956e037d3d5bea0a02153887abfafe2f Mon Sep 17 00:00:00 2001 From: Brandon Date: Sun, 17 Mar 2024 08:55:15 +0100 Subject: [PATCH 040/108] feat: add `banner` to `ClientUser.edit` (#2396) Signed-off-by: Brandon Signed-off-by: plun1331 Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 --- CHANGELOG.md | 5 +++++ discord/user.py | 23 +++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac49862f6b..ea978f2d25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ possible (see our [Version Guarantees] for more info). These changes are available on the `master` branch, but have not yet been released. +### Added + +- Added `banner` parameter to `ClientUser.edit`. + ([#2396](https://github.com/Pycord-Development/pycord/pull/2396)) + ### Fixed - Fixed the type-hinting of `Member.move_to` and `Member.edit` to reflect actual diff --git a/discord/user.py b/discord/user.py index 1f11574a1c..5cd0352df3 100644 --- a/discord/user.py +++ b/discord/user.py @@ -422,7 +422,11 @@ def _update(self, data: UserPayload) -> None: # TODO: Username might not be able to edit anymore. async def edit( - self, *, username: str = MISSING, avatar: bytes = MISSING + self, + *, + username: str = MISSING, + avatar: bytes = MISSING, + banner: bytes = MISSING, ) -> ClientUser: """|coro| @@ -430,16 +434,19 @@ async def edit( .. note:: - To upload an avatar, a :term:`py:bytes-like object` must be passed in that + To upload an avatar or banner, a :term:`py:bytes-like object` must be passed in that represents the image being uploaded. If this is done through a file then the file must be opened via ``open('some_filename', 'rb')`` and the :term:`py:bytes-like object` is given through the use of ``fp.read()``. - The only image formats supported for uploading is JPEG and PNG. + The only image formats supported for uploading are JPEG, PNG, and GIF. .. versionchanged:: 2.0 The edit is no longer in-place, instead the newly edited client user is returned. + .. versionchanged:: 2.6 + The ``banner`` keyword-only parameter was added. + Parameters ---------- username: :class:`str` @@ -447,6 +454,9 @@ async def edit( avatar: :class:`bytes` A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no avatar. + banner: :class:`bytes` + A :term:`py:bytes-like object` representing the image to upload. + Could be ``None`` to denote no banner. Returns ------- @@ -458,7 +468,7 @@ async def edit( HTTPException Editing your profile failed. InvalidArgument - Wrong image format passed for ``avatar``. + Wrong image format passed for ``avatar`` or ``banner``. """ payload: dict[str, Any] = {} if username is not MISSING: @@ -469,6 +479,11 @@ async def edit( elif avatar is not MISSING: payload["avatar"] = _bytes_to_base64_data(avatar) + if banner is None: + payload["banner"] = None + elif banner is not MISSING: + payload["banner"] = _bytes_to_base64_data(banner) + data: UserPayload = await self._state.http.edit_profile(payload) return ClientUser(state=self._state, data=data) From bfe7827ddda96b07af5f8d5fbdf7a808338b6795 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:10:02 +0000 Subject: [PATCH 041/108] chore(pre-commit): pre-commit autoupdate (#2398) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/autoflake: v2.3.0 → v2.3.1](https://github.com/PyCQA/autoflake/compare/v2.3.0...v2.3.1) - [github.com/psf/black: 24.2.0 → 24.3.0](https://github.com/psf/black/compare/24.2.0...24.3.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2660004bbf..81e9a64c72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - repo: https://github.com/PyCQA/autoflake - rev: v2.3.0 + rev: v2.3.1 hooks: - id: autoflake # args: @@ -28,7 +28,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.3.0 hooks: - id: black args: [--safe, --quiet] From ca070544493807fade3f7a286cf9a311ec721856 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:17:24 +0000 Subject: [PATCH 042/108] chore(deps-dev): update pytest requirement from ~=8.0.2 to ~=8.1.1 (#2391) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.2...8.1.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: plun1331 --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 2822060957..2919fd2767 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ -r _.txt pylint~=3.1.0 -pytest~=8.0.2 +pytest~=8.1.1 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 mypy~=1.9.0 From 43a9ee776d5201f957f84fce130da203f189381d Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:25:34 +0000 Subject: [PATCH 043/108] fix: add user argument to paginator.edit (#2390) * patch paginator.edit * style(pre-commit): auto fixes from pre-commit.com hooks * changelog * style(pre-commit): auto fixes from pre-commit.com hooks * Update CHANGELOG.md --------- Signed-off-by: plun1331 Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 --- CHANGELOG.md | 4 ++++ discord/ext/pages/pagination.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea978f2d25..d3f5595f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ These changes are available on the `master` branch, but have not yet been releas - Added `banner` parameter to `ClientUser.edit`. ([#2396](https://github.com/Pycord-Development/pycord/pull/2396)) +- Added `user` argument to `Paginator.edit`. + ([#2390](https://github.com/Pycord-Development/pycord/pull/2390)) ### Fixed @@ -22,6 +24,8 @@ These changes are available on the `master` branch, but have not yet been releas - Fixed a deprecation warning from being displayed when running `python -m discord -v` by replacing the deprecated module. ([#2392](https://github.com/Pycord-Development/pycord/pull/2392)) +- Fixed `Paginator.edit` to no longer set user to the bot. + ([#2390](https://github.com/Pycord-Development/pycord/pull/2390)) ### Changed diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py index 8108d12976..e3eca0cf4d 100644 --- a/discord/ext/pages/pagination.py +++ b/discord/ext/pages/pagination.py @@ -29,6 +29,8 @@ import discord from discord.ext.bridge import BridgeContext from discord.ext.commands import Context +from discord.member import Member +from discord.user import User __all__ = ( "PaginatorButton", @@ -1035,6 +1037,7 @@ async def edit( suppress: bool | None = None, allowed_mentions: discord.AllowedMentions | None = None, delete_after: float | None = None, + user: User | Member | None = None, ) -> discord.Message | None: """Edits an existing message to replace it with the paginator contents. @@ -1060,6 +1063,8 @@ async def edit( are used instead. delete_after: Optional[:class:`float`] If set, deletes the paginator after the specified time. + user: Optional[Union[:class:`~discord.User`, :class:`~discord.Member`]] + If set, changes the user that this paginator belongs to. Returns ------- @@ -1079,7 +1084,7 @@ async def edit( if page_content.custom_view: self.update_custom_view(page_content.custom_view) - self.user = message.author + self.user = user or self.user try: self.message = await message.edit( From 6f1ad85f95aed5c8981529e0dec50ced45c08753 Mon Sep 17 00:00:00 2001 From: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Date: Wed, 20 Mar 2024 08:12:09 -0700 Subject: [PATCH 044/108] docs: fix typo (#2401) --- discord/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/bot.py b/discord/bot.py index 6643169ad5..7d561b52cc 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -680,7 +680,7 @@ 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 such as the follwing: + :func:`.on_connect` event, then you should invoke this coroutine as well such as the following: .. code-block:: python From 7a6fcda419bee88337679edf0d9e2e15308eca5a Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 21 Mar 2024 03:10:12 +0000 Subject: [PATCH 045/108] fix: NameError in interaction edge cases (#2402) * move guild import * style(pre-commit): auto fixes from pre-commit.com hooks * changelog --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/interactions.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3f5595f10..34e6bef80d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2392](https://github.com/Pycord-Development/pycord/pull/2392)) - Fixed `Paginator.edit` to no longer set user to the bot. ([#2390](https://github.com/Pycord-Development/pycord/pull/2390)) +- Fixed `NameError` in some instances of `Interaction`. + ([#2402](https://github.com/Pycord-Development/pycord/pull/2402)) ### Changed diff --git a/discord/interactions.py b/discord/interactions.py index 0e254d514a..a4222ca5d6 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -34,6 +34,7 @@ from .errors import ClientException, InteractionResponded, InvalidArgument from .file import File from .flags import MessageFlags +from .guild import Guild from .member import Member from .message import Attachment, Message from .monetization import Entitlement @@ -69,7 +70,6 @@ from .client import Client from .commands import OptionChoice from .embeds import Embed - from .guild import Guild from .mentions import AllowedMentions from .state import ConnectionState from .threads import Thread From 532db98b9594dfa65c51d92c5cebbfd7ca024e05 Mon Sep 17 00:00:00 2001 From: DarkSoul <83767949+D4rk-S0ul@users.noreply.github.com> Date: Fri, 22 Mar 2024 07:23:34 +0100 Subject: [PATCH 046/108] feat: retry http requests that fail with a 503 status (#2395) Signed-off-by: DarkSoul <83767949+D4rk-S0ul@users.noreply.github.com> Co-authored-by: plun1331 Co-authored-by: Lala Sabathil --- CHANGELOG.md | 2 ++ discord/http.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34e6bef80d..b467fec9e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ These changes are available on the `master` branch, but have not yet been releas - Changed the type of `Guild.bitrate_limit` to `int`. ([#2387](https://github.com/Pycord-Development/pycord/pull/2387)) +- HTTP requests that fail with a 503 status are now re-tried. + ([#2395](https://github.com/Pycord-Development/pycord/pull/2395)) ## [2.5.0] - 2024-03-02 diff --git a/discord/http.py b/discord/http.py index ecbebfdb87..a8833d17af 100644 --- a/discord/http.py +++ b/discord/http.py @@ -357,8 +357,8 @@ async def request( continue - # we've received a 500, 502, or 504, unconditional retry - if response.status in {500, 502, 504}: + # we've received a 500, 502, 503, or 504, unconditional retry + if response.status in {500, 502, 503, 504}: await asyncio.sleep(1 + tries * 2) continue From a29bc142724a8be29ac10450b7bde1eb16a659e8 Mon Sep 17 00:00:00 2001 From: YoggieS <61660055+yoggys@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:14:52 +0100 Subject: [PATCH 047/108] fix: type-hinting in ScheduledEvent.subscribers (#2400) --- CHANGELOG.md | 2 ++ discord/iterators.py | 6 +++--- discord/scheduled_events.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b467fec9e2..bd19979e74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2390](https://github.com/Pycord-Development/pycord/pull/2390)) - Fixed `NameError` in some instances of `Interaction`. ([#2402](https://github.com/Pycord-Development/pycord/pull/2402)) +- Fixed the type-hinting of `ScheduledEvent.subscribers` to reflect actual behavior. + ([#2400](https://github.com/Pycord-Development/pycord/pull/2400)) ### Changed diff --git a/discord/iterators.py b/discord/iterators.py index de442aa0fb..ef7ef9be85 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -852,10 +852,10 @@ class ScheduledEventSubscribersIterator(_AsyncIterator[Union["User", "Member"]]) def __init__( self, event: ScheduledEvent, - limit: int, + limit: int | None, with_member: bool = False, - before: datetime.datetime | int = None, - after: datetime.datetime | int = None, + before: datetime.datetime | int | None = None, + after: datetime.datetime | int | None = None, ): if isinstance(before, datetime.datetime): before = Object(id=time_snowflake(before, high=False)) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index cda13f48b9..4ba16bcdec 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -482,11 +482,11 @@ async def cancel(self, *, reason: str | None = None) -> None: def subscribers( self, *, - limit: int = 100, + limit: int | None = 100, as_member: bool = False, before: Snowflake | datetime.datetime | None = None, after: Snowflake | datetime.datetime | None = None, - ) -> AsyncIterator: + ) -> ScheduledEventSubscribersIterator: """Returns an :class:`AsyncIterator` representing the users or members subscribed to the event. The ``after`` and ``before`` parameters must represent member From 548ca852ef7e1db277efccab0b77dce29ba82189 Mon Sep 17 00:00:00 2001 From: YoggieS <61660055+yoggys@users.noreply.github.com> Date: Sat, 23 Mar 2024 20:38:40 +0100 Subject: [PATCH 048/108] fix: TypeError when `ScheduledEvent.subscribers` limit is None (#2407) --- CHANGELOG.md | 2 ++ discord/iterators.py | 10 ++++++++-- discord/types/scheduled_events.py | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd19979e74..bbf54d9c22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2402](https://github.com/Pycord-Development/pycord/pull/2402)) - Fixed the type-hinting of `ScheduledEvent.subscribers` to reflect actual behavior. ([#2400](https://github.com/Pycord-Development/pycord/pull/2400)) +- Fixed `ScheduledEvent.subscribers` behavior with `limit=None`. + ([#2407](https://github.com/Pycord-Development/pycord/pull/2407)) ### Changed diff --git a/discord/iterators.py b/discord/iterators.py index ef7ef9be85..1537dc8605 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -919,8 +919,14 @@ async def fill_subs(self): before=before, after=after, ) - if data: - self.limit -= self.retrieve + + data_length = len(data) + if data_length < self.retrieve: + self.limit = 0 + elif data_length > 0: + if self.limit: + self.limit -= self.retrieve + self.after = Object(id=int(data[-1]["user_id"])) for element in reversed(data): if "member" in element: diff --git a/discord/types/scheduled_events.py b/discord/types/scheduled_events.py index 85d77d7313..9bb4ad0328 100644 --- a/discord/types/scheduled_events.py +++ b/discord/types/scheduled_events.py @@ -60,5 +60,6 @@ class ScheduledEventEntityMetadata(TypedDict): class ScheduledEventSubscriber(TypedDict): guild_scheduled_event_id: Snowflake + user_id: Snowflake user: User member: Member | None From ac7c3411f6cf201442ef549d200f9350ba52ea47 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:00:51 +0000 Subject: [PATCH 049/108] feat: add once kwarg to Cog.listener (#2403) * add once kwarg to Cog.listener * clarify * style(pre-commit): auto fixes from pre-commit.com hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil --- discord/cog.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/discord/cog.py b/discord/cog.py index 24a5b58e70..4f064edb26 100644 --- a/discord/cog.py +++ b/discord/cog.py @@ -379,7 +379,9 @@ def _get_overridden_method(cls, method: FuncT) -> FuncT | None: ) @classmethod - def listener(cls, name: str = MISSING) -> Callable[[FuncT], FuncT]: + def listener( + cls, name: str = MISSING, once: bool = False + ) -> Callable[[FuncT], FuncT]: """A decorator that marks a function as a listener. This is the cog equivalent of :meth:`.Bot.listen`. @@ -389,6 +391,9 @@ def listener(cls, name: str = MISSING) -> Callable[[FuncT], FuncT]: name: :class:`str` The name of the event being listened to. If not provided, it defaults to the function's name. + once: :class:`bool` + If this listener should only be called once after each cog load. + Defaults to false. Raises ------ @@ -411,6 +416,7 @@ def decorator(func: FuncT) -> FuncT: raise TypeError("Listener function must be a coroutine function.") actual.__cog_listener__ = True to_assign = name or actual.__name__ + actual._once = once try: actual.__cog_listener_names__.append(to_assign) except AttributeError: From fbd9d7e036de7051b1dbb743d271f8da659db61b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 11:52:41 +0100 Subject: [PATCH 050/108] chore(pre-commit): pre-commit autoupdate (#2410) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.1 → v3.15.2](https://github.com/asottile/pyupgrade/compare/v3.15.1...v3.15.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81e9a64c72..8838c9a079 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: # - --remove-duplicate-keys # - --remove-unused-variables - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 + rev: v3.15.2 hooks: - id: pyupgrade args: [--py38-plus] From 8cec636e10dfe2c6aa09156a9a5626f796b54a4d Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 30 Mar 2024 06:51:20 +0000 Subject: [PATCH 051/108] fix: pass proper state to interaction._guild (#2411) --- CHANGELOG.md | 2 ++ discord/interactions.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbf54d9c22..8d22818f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2400](https://github.com/Pycord-Development/pycord/pull/2400)) - Fixed `ScheduledEvent.subscribers` behavior with `limit=None`. ([#2407](https://github.com/Pycord-Development/pycord/pull/2407)) +- Fixed invalid data being passed to `Interaction._guild` in certain cases. + ([#2411](https://github.com/Pycord-Development/pycord/pull/2411)) ### Changed diff --git a/discord/interactions.py b/discord/interactions.py index a4222ca5d6..1362acfac4 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -198,7 +198,7 @@ def _from_data(self, data: InteractionPayload): self._guild: Guild | None = None self._guild_data = data.get("guild") if self.guild is None and self._guild_data: - self._guild = Guild(data=self._guild_data, state=self) + self._guild = Guild(data=self._guild_data, state=self._state) # TODO: there's a potential data loss here if self.guild_id: From bb61cf7cff5531d93a1b1b94670907e1bb54363b Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:09:12 +0100 Subject: [PATCH 052/108] fix: option and BridgeOption adjustments (#2417) --- CHANGELOG.md | 8 ++++++++ discord/commands/options.py | 15 ++++++++------- discord/ext/bridge/core.py | 38 ++++++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d22818f1d..940069b33a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2396](https://github.com/Pycord-Development/pycord/pull/2396)) - Added `user` argument to `Paginator.edit`. ([#2390](https://github.com/Pycord-Development/pycord/pull/2390)) +- Added `bridge_option` decorator. Required for `bridge.Bot` in 2.7. + ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) ### Fixed @@ -34,6 +36,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2407](https://github.com/Pycord-Development/pycord/pull/2407)) - Fixed invalid data being passed to `Interaction._guild` in certain cases. ([#2411](https://github.com/Pycord-Development/pycord/pull/2411)) +- Fixed option typehints being ignored when using `parameter_name`. + ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) ### Changed @@ -41,6 +45,10 @@ These changes are available on the `master` branch, but have not yet been releas ([#2387](https://github.com/Pycord-Development/pycord/pull/2387)) - HTTP requests that fail with a 503 status are now re-tried. ([#2395](https://github.com/Pycord-Development/pycord/pull/2395)) +- `option` decorator now accepts `input_type`. + ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) +- `Option` may be used instead of `BridgeOption` until 2.7. + ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) ## [2.5.0] - 2024-03-02 diff --git a/discord/commands/options.py b/discord/commands/options.py index 80d525c65b..38cac6b539 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -395,7 +395,7 @@ def to_dict(self) -> dict[str, str | int | float]: return as_dict -def option(name, type=None, **kwargs): +def option(name, input_type=None, **kwargs): """A decorator that can be used instead of typehinting :class:`.Option`. .. versionadded:: 2.0 @@ -408,12 +408,13 @@ def option(name, type=None, **kwargs): """ def decorator(func): - nonlocal type - type = type or func.__annotations__.get(name, str) - if parameter := kwargs.get("parameter_name"): - func.__annotations__[parameter] = Option(type, name=name, **kwargs) - else: - func.__annotations__[name] = Option(type, **kwargs) + resolved_name = kwargs.pop("parameter_name", None) or name + itype = ( + kwargs.pop("type", None) + or input_type + or func.__annotations__.get(resolved_name, str) + ) + func.__annotations__[resolved_name] = Option(itype, name=name, **kwargs) return func return decorator diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py index b1a37b3980..c14d9db95a 100644 --- a/discord/ext/bridge/core.py +++ b/discord/ext/bridge/core.py @@ -40,7 +40,7 @@ SlashCommandOptionType, ) -from ...utils import MISSING, find, get +from ...utils import MISSING, find, get, warn_deprecated from ..commands import BadArgument from ..commands import Bot as ExtBot from ..commands import ( @@ -63,6 +63,7 @@ "BridgeCommandGroup", "bridge_command", "bridge_group", + "bridge_option", "BridgeExtCommand", "BridgeSlashCommand", "BridgeExtGroup", @@ -627,3 +628,38 @@ async def convert(self, ctx, argument: str) -> Any: return converted except ValueError as exc: raise BadArgument() from exc + + +def bridge_option(name, input_type=None, **kwargs): + """A decorator that can be used instead of typehinting :class:`.BridgeOption`. + + .. versionadded:: 2.6 + + Attributes + ---------- + parameter_name: :class:`str` + The name of the target parameter this option is mapped to. + This allows you to have a separate UI ``name`` and parameter name. + """ + + def decorator(func): + resolved_name = kwargs.pop("parameter_name", None) or name + itype = ( + kwargs.pop("type", None) + or input_type + or func.__annotations__.get(resolved_name, str) + ) + func.__annotations__[resolved_name] = BridgeOption(itype, name=name, **kwargs) + return func + + return decorator + + +discord.commands.options.Option = BridgeOption +discord.Option = BridgeOption +warn_deprecated( + "Option", + "BridgeOption", + "2.5", + reference="https://github.com/Pycord-Development/pycord/pull/2417", +) From 59b9b32b7d6044158c45e3d9b4c4ad346618f443 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 08:14:16 +0000 Subject: [PATCH 053/108] chore(pre-commit): pre-commit autoupdate (#2416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8838c9a079..7ca67dc12b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 90d27c56f93a12dc20005edec90a4f2dece6c905 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 12 Apr 2024 06:43:32 +0100 Subject: [PATCH 054/108] feat: implement Guild.search_members (#2418) * implement guild member search * clarification. * style(pre-commit): auto fixes from pre-commit.com hooks * cl * undo iteration * style(pre-commit): auto fixes from pre-commit.com hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/guild.py | 30 ++++++++++++++++++++++++++++++ discord/http.py | 14 ++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 940069b33a..0ae1911b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2390](https://github.com/Pycord-Development/pycord/pull/2390)) - Added `bridge_option` decorator. Required for `bridge.Bot` in 2.7. ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) +- Added `Guild.search_members`. + ([#2418](https://github.com/Pycord-Development/pycord/pull/2418)) ### Fixed diff --git a/discord/guild.py b/discord/guild.py index d1044cf3f8..2b6ff7a382 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2027,6 +2027,36 @@ def fetch_members( return MemberIterator(self, limit=limit, after=after) + async def search_members(self, query: str, *, limit: int = 1000) -> list[Member]: + """Search for guild members whose usernames or nicknames start with the query string. Unlike :meth:`fetch_members`, this does not require :meth:`Intents.members`. + + .. note:: + + This method is an API call. For general usage, consider filtering :attr:`members` instead. + + .. versionadded:: 2.6 + + Parameters + ---------- + query: :class:`str` + Searches for usernames and nicknames that start with this string, case-insensitive. + limit: Optional[:class:`int`] + The maximum number of members to retrieve, up to 1000. + + Returns + ------- + List[:class:`Member`] + The list of members that have matched the query. + + Raises + ------ + HTTPException + Getting the members failed. + """ + + data = await self._state.http.search_members(self.id, query, limit) + return [Member(data=m, guild=self, state=self._state) for m in data] + async def fetch_member(self, member_id: int, /) -> Member: """|coro| diff --git a/discord/http.py b/discord/http.py index a8833d17af..e79d6120b7 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1636,6 +1636,20 @@ def get_members( r = Route("GET", "/guilds/{guild_id}/members", guild_id=guild_id) return self.request(r, params=params) + def search_members( + self, + guild_id: Snowflake, + query: str, + limit: int, + ) -> Response[list[member.MemberWithUser]]: + params: dict[str, Any] = { + "query": query, + "limit": limit, + } + + r = Route("GET", "/guilds/{guild_id}/members/search", guild_id=guild_id) + return self.request(r, params=params) + def get_member( self, guild_id: Snowflake, member_id: Snowflake ) -> Response[member.MemberWithUser]: From 2df4ac95deb3546bb1ab4adb328046c6d95e08f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:31:56 +0000 Subject: [PATCH 055/108] ci(deps): bump ribtoks/tdg-github-action from 0.4.7.pre.beta to 0.4.10.pre.beta (#2420) ci(deps): bump ribtoks/tdg-github-action Bumps [ribtoks/tdg-github-action](https://github.com/ribtoks/tdg-github-action) from 0.4.7.pre.beta to 0.4.10.pre.beta. - [Release notes](https://github.com/ribtoks/tdg-github-action/releases) - [Commits](https://github.com/ribtoks/tdg-github-action/compare/v0.4.7-beta...v0.4.10-beta) --- updated-dependencies: - dependency-name: ribtoks/tdg-github-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: plun1331 --- .github/workflows/todo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml index 508f34218f..6f4ba8a5b6 100644 --- a/.github/workflows/todo.yml +++ b/.github/workflows/todo.yml @@ -6,7 +6,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run tdg-github-action - uses: ribtoks/tdg-github-action@v0.4.7-beta + uses: ribtoks/tdg-github-action@v0.4.10-beta with: TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} From c10022cc7be5cd4bcbbacc9b68f4053c01064a09 Mon Sep 17 00:00:00 2001 From: jordan-day <47164307+jordan-day@users.noreply.github.com> Date: Mon, 15 Apr 2024 04:09:52 -0700 Subject: [PATCH 056/108] fix: Added member data to the raw_reaction_remove event (#2412) * fix: Added member data to the raw_reaction_remove event * style(pre-commit): auto fixes from pre-commit.com hooks * Added changelog.md for adding member data to raw reaction remove * Update CHANGELOG.md Signed-off-by: plun1331 * doc: Updated documentation for the RawReactionActionEvent.member field --------- Signed-off-by: plun1331 Co-authored-by: root Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 --- CHANGELOG.md | 2 ++ discord/raw_models.py | 3 +-- discord/state.py | 11 +++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae1911b88..2bdd10e65d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) - Added `Guild.search_members`. ([#2418](https://github.com/Pycord-Development/pycord/pull/2418)) +- Added `member` data to the `raw_reaction_remove` event. + ([#2412](https://github.com/Pycord-Development/pycord/pull/2412)) ### Fixed diff --git a/discord/raw_models.py b/discord/raw_models.py index 49f5575064..e3d38e558d 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -205,8 +205,7 @@ class RawReactionActionEvent(_RawReprMixin): emoji: :class:`PartialEmoji` The custom or unicode emoji being used. member: Optional[:class:`Member`] - The member who added the reaction. Only available if `event_type` is `REACTION_ADD` - and the reaction is inside a guild. + The member who added the reaction. Only available if the reaction occurs within a guild. .. versionadded:: 1.3 diff --git a/discord/state.py b/discord/state.py index 5b9aea0e35..0f9a6dc438 100644 --- a/discord/state.py +++ b/discord/state.py @@ -783,6 +783,17 @@ def parse_message_reaction_remove(self, data) -> None: emoji_id = utils._get_as_snowflake(emoji, "id") emoji = PartialEmoji.with_state(self, id=emoji_id, name=emoji["name"]) raw = RawReactionActionEvent(data, emoji, "REACTION_REMOVE") + + member_data = data.get("member") + if member_data: + guild = self._get_guild(raw.guild_id) + if guild is not None: + raw.member = Member(data=member_data, guild=guild, state=self) + else: + raw.member = None + else: + raw.member = None + self.dispatch("raw_reaction_remove", raw) message = self._get_message(raw.message_id) From 7a7f3649cf6920edb8a6ca39505ea1c16e6f16c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 02:14:35 +0000 Subject: [PATCH 057/108] chore(pre-commit): pre-commit autoupdate (#2422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.3.0 → 24.4.0](https://github.com/psf/black/compare/24.3.0...24.4.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7ca67dc12b..9719862ee2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 24.4.0 hooks: - id: black args: [--safe, --quiet] From 929e15ef71752a8ca4ce2daa55ad6022e5aa1365 Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:10:58 +0300 Subject: [PATCH 058/108] docs: remove false optional (#2424) docs: not optional --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 2b6ff7a382..6632743105 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2040,7 +2040,7 @@ async def search_members(self, query: str, *, limit: int = 1000) -> list[Member] ---------- query: :class:`str` Searches for usernames and nicknames that start with this string, case-insensitive. - limit: Optional[:class:`int`] + limit: :class:`int` The maximum number of members to retrieve, up to 1000. Returns From f5683c5f56b1c1692b010ff9f775fd536d0c9cab Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:01:15 +0100 Subject: [PATCH 059/108] fix: Guild.query_members may accept empty query and limit (#2419) Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: plun1331 --- CHANGELOG.md | 2 ++ discord/guild.py | 35 ++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bdd10e65d..9c6e30c8a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) - `Option` may be used instead of `BridgeOption` until 2.7. ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) +- `Guild.query_members` now accepts `limit=None` to retrieve all members. + ([#2419](https://github.com/Pycord-Development/pycord/pull/2419)) ## [2.5.0] - 2024-03-02 diff --git a/discord/guild.py b/discord/guild.py index 6632743105..d94eb2df83 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3361,7 +3361,7 @@ async def query_members( self, query: str | None = None, *, - limit: int = 5, + limit: int | None = 5, user_ids: list[int] | None = None, presences: bool = False, cache: bool = True, @@ -3379,10 +3379,14 @@ async def query_members( ---------- query: Optional[:class:`str`] The string that the username's start with. - limit: :class:`int` - The maximum number of members to send back. This must be - a number between 5 and 100. - presences: :class:`bool` + user_ids: Optional[List[:class:`int`]] + List of user IDs to search for. If the user ID is not in the guild then it won't be returned. + + .. versionadded:: 1.4 + limit: Optional[:class:`int`] + The maximum number of members to send back. If no query is passed, passing ``None`` returns all members. + If a ``query`` or ``user_ids`` is passed, must be between 1 and 100. Defaults to 5. + presences: Optional[:class:`bool`] Whether to request for presences to be provided. This defaults to ``False``. @@ -3390,11 +3394,7 @@ async def query_members( cache: :class:`bool` Whether to cache the members internally. This makes operations - such as :meth:`get_member` work for those that matched. - user_ids: Optional[List[:class:`int`]] - List of user IDs to search for. If the user ID is not in the guild then it won't be returned. - - .. versionadded:: 1.4 + such as :meth:`get_member` work for those that matched. Defaults to ``True``. Returns ------- @@ -3414,12 +3414,18 @@ async def query_members( if presences and not self._state._intents.presences: raise ClientException("Intents.presences must be enabled to use this.") - if query is None: - if query == "": - raise ValueError("Cannot pass empty query string.") + if not limit or limit > 100 or limit < 1: + if query or user_ids: + raise ValueError( + "limit must be between 1 and 100 when using query or user_ids" + ) + if not limit: + query = "" + limit = 0 + if query is None: if user_ids is None: - raise ValueError("Must pass either query or user_ids") + raise ValueError("Must pass query or user_ids, or set limit to None") if user_ids is not None and query is not None: raise ValueError("Cannot pass both query and user_ids") @@ -3427,7 +3433,6 @@ async def query_members( if user_ids is not None and not user_ids: raise ValueError("user_ids must contain at least 1 value") - limit = min(100, limit or 5) return await self._state.query_members( self, query=query, From fe0a5f49d60443ef7581e92f508f50e6c5e61188 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Thu, 25 Apr 2024 16:16:33 +0200 Subject: [PATCH 060/108] fix(actions): fallback to macos-13 in test suite for python 3.8 & 3.9 (#2437) * remove macos from test suit Signed-off-by: Lala Sabathil * Update test.yml Signed-off-by: Lala Sabathil * ci: use macos-latest but revert to osx13 on some Signed-off-by: BobDotCom <71356958+BobDotCom@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks --------- Signed-off-by: Lala Sabathil Signed-off-by: BobDotCom <71356958+BobDotCom@users.noreply.github.com> Co-authored-by: BobDotCom <71356958+BobDotCom@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa77843552..9c500580ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,15 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + # Python 3.8/3.9 are on macos-13 but not macos-latest (macos-14-arm64) + # https://github.com/actions/setup-python/issues/696#issuecomment-1637587760 + exclude: + - { python-version: "3.8", os: "macos-latest" } + - { python-version: "3.9", os: "macos-latest" } + include: + - { python-version: "3.8", os: "macos-13" } + - { python-version: "3.9", os: "macos-13" } + env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }} From 65d9aafe9186b1455a7dcad4a34be679f7a8b3aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:27:28 -0500 Subject: [PATCH 061/108] chore(deps-dev): update mypy requirement from ~=1.9.0 to ~=1.10.0 (#2434) Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/1.9.0...v1.10.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 2919fd2767..1b176705d0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -3,7 +3,7 @@ pylint~=3.1.0 pytest~=8.1.1 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 -mypy~=1.9.0 +mypy~=1.10.0 coverage~=7.4 pre-commit==3.5.0 codespell==2.2.6 From c12670a9f4705f485a864b9552e7634c3849dfa5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:38:15 -0500 Subject: [PATCH 062/108] chore(deps-dev): update coverage requirement from ~=7.4 to ~=7.5 (#2432) Updates the requirements on [coverage](https://github.com/nedbat/coveragepy) to permit the latest version. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/7.4.0...7.5.0) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 1b176705d0..476607969f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,7 +4,7 @@ pytest~=8.1.1 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 mypy~=1.10.0 -coverage~=7.4 +coverage~=7.5 pre-commit==3.5.0 codespell==2.2.6 bandit==1.7.8 From 0536899144a1d912707ca603337767a5b98338e5 Mon Sep 17 00:00:00 2001 From: Jesse Date: Fri, 26 Apr 2024 15:33:21 +0300 Subject: [PATCH 063/108] fix: Raise WebSocketClosure on protocol level errors (#2435) --- discord/gateway.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/discord/gateway.py b/discord/gateway.py index 1219c48831..7a765405af 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -603,13 +603,11 @@ async def poll_event(self): await self.received_message(msg.data) elif msg.type is aiohttp.WSMsgType.BINARY: await self.received_message(msg.data) - elif msg.type is aiohttp.WSMsgType.ERROR: - _log.debug("Received %s", msg) - raise msg.data elif msg.type in ( aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSE, + aiohttp.WSMsgType.ERROR, ): _log.debug("Received %s", msg) raise WebSocketClosure From a1439bab393c3014a1c815bd9b76292cf0a96abb Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:04:23 +0100 Subject: [PATCH 064/108] feat: Support bulk banning in guilds (#2421) Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 Co-authored-by: Lala Sabathil --- CHANGELOG.md | 8 ++++ discord/guild.py | 80 ++++++++++++++++++++++++++++++---- discord/http.py | 29 ++++++++---- discord/member.py | 4 +- discord/types/guild.py | 5 +++ docs/ext/commands/commands.rst | 9 ++-- 6 files changed, 109 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6e30c8a5..39df9290f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) - Added `Guild.search_members`. ([#2418](https://github.com/Pycord-Development/pycord/pull/2418)) +- Added bulk banning up to 200 users through `Guild.bulk_ban`. + ([#2421](https://github.com/Pycord-Development/pycord/pull/2421)) - Added `member` data to the `raw_reaction_remove` event. ([#2412](https://github.com/Pycord-Development/pycord/pull/2412)) @@ -56,6 +58,12 @@ These changes are available on the `master` branch, but have not yet been releas - `Guild.query_members` now accepts `limit=None` to retrieve all members. ([#2419](https://github.com/Pycord-Development/pycord/pull/2419)) +### Removed + +- Removed the `delete_message_days` parameter from ban methods. Please use + `delete_message_seconds` instead. + ([#2421](https://github.com/Pycord-Development/pycord/pull/2421)) + ## [2.5.0] - 2024-03-02 ### Added diff --git a/discord/guild.py b/discord/guild.py index d94eb2df83..f015048f08 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -32,7 +32,6 @@ Any, ClassVar, List, - Literal, NamedTuple, Optional, Sequence, @@ -3076,7 +3075,6 @@ async def ban( user: Snowflake, *, delete_message_seconds: int | None = None, - delete_message_days: Literal[0, 1, 2, 3, 4, 5, 6, 7] | None = None, reason: str | None = None, ) -> None: """|coro| @@ -3096,9 +3094,6 @@ async def ban( The number of seconds worth of messages to delete from the user in the guild. The minimum is 0 and the maximum is 604800 (i.e. 7 days). The default is 0. - delete_message_days: Optional[:class:`int`] - ***Deprecated parameter***, same as ``delete_message_seconds`` but - is used for days instead. reason: Optional[:class:`str`] The reason the user got banned. @@ -3109,11 +3104,67 @@ async def ban( HTTPException Banning failed. """ - if delete_message_seconds and delete_message_days: + + if delete_message_seconds is not None and not ( + 0 <= delete_message_seconds <= 604800 + ): raise TypeError( - "delete_message_seconds and delete_message_days are mutually exclusive." + "delete_message_seconds must be between 0 and 604800 seconds." ) + await self._state.http.ban( + user.id, self.id, delete_message_seconds, reason=reason + ) + + async def bulk_ban( + self, + *users: Snowflake, + delete_message_seconds: int | None = None, + reason: str | None = None, + ) -> list[list[Snowflake], list[Snowflake]]: + r"""|coro| + + Bulk ban users from the guild. + + The users must meet the :class:`abc.Snowflake` abc. + + You must have the :attr:`~Permissions.ban_members` permission to + do this. + + Example Usage: :: + + # Ban multiple users + successes, failures = await guild.ban(user1, user2, user3, ..., reason="Raid") + + # Ban a list of users + successes, failures = await guild.ban(*users) + + Parameters + ---------- + \*users: :class:`abc.Snowflake` + An argument list of users to ban from the guild, up to 200. + delete_message_seconds: Optional[:class:`int`] + The number of seconds worth of messages to delete from + the user in the guild. The minimum is 0 and the maximum + is 604800 (i.e. 7 days). The default is 0. + reason: Optional[:class:`str`] + The reason the users were banned. + + Returns + ------- + List[List[:class:`abc.Snowflake`], List[:class:`abc.Snowflake`]] + Returns two lists: the first contains members that were successfully banned, while the second is members that could not be banned. + + Raises + ------ + ValueError + You tried to ban more than 200 users. + Forbidden + You do not have the proper permissions to ban. + HTTPException + No users were banned. + """ + if delete_message_seconds is not None and not ( 0 <= delete_message_seconds <= 604800 ): @@ -3121,9 +3172,20 @@ async def ban( "delete_message_seconds must be between 0 and 604800 seconds." ) - await self._state.http.ban( - user.id, self.id, delete_message_seconds, delete_message_days, reason=reason + if len(users) > 200 or len(users) < 1: + raise ValueError( + "The number of users to be banned must be between 1 and 200." + ) + + data = await self._state.http.bulk_ban( + [u.id for u in users], + self.id, + delete_message_seconds, + reason=reason, ) + banned = [u for u in users if str(u.id) in data["banned_users"]] + failed = [u for u in users if str(u.id) in data["failed_users"]] + return banned, failed async def unban(self, user: Snowflake, *, reason: str | None = None) -> None: """|coro| diff --git a/discord/http.py b/discord/http.py index e79d6120b7..3622afc2bd 100644 --- a/discord/http.py +++ b/discord/http.py @@ -907,7 +907,6 @@ def ban( user_id: Snowflake, guild_id: Snowflake, delete_message_seconds: int = None, - delete_message_days: int = None, reason: str | None = None, ) -> Response[None]: r = Route( @@ -920,17 +919,29 @@ def ban( if delete_message_seconds: params["delete_message_seconds"] = delete_message_seconds - elif delete_message_days: - warn_deprecated( - "delete_message_days", - "delete_message_seconds", - "2.2", - reference="https://github.com/discord/discord-api-docs/pull/5219", - ) - params["delete_message_days"] = delete_message_days return self.request(r, params=params, reason=reason) + def bulk_ban( + self, + user_ids: list[Snowflake], + guild_id: Snowflake, + delete_message_seconds: int = None, + reason: str | None = None, + ) -> Response[guild.GuildBulkBan]: + r = Route( + "POST", + "/guilds/{guild_id}/bulk-ban", + guild_id=guild_id, + ) + payload = { + "user_ids": user_ids, + } + if delete_message_seconds: + payload["delete_message_seconds"] = delete_message_seconds + + return self.request(r, json=payload, reason=reason) + def unban( self, user_id: Snowflake, guild_id: Snowflake, *, reason: str | None = None ) -> Response[None]: diff --git a/discord/member.py b/discord/member.py index fecb9b9dfa..c854e12979 100644 --- a/discord/member.py +++ b/discord/member.py @@ -30,7 +30,7 @@ import itertools import sys from operator import attrgetter -from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union +from typing import TYPE_CHECKING, Any, TypeVar, Union import discord.abc @@ -684,7 +684,6 @@ async def ban( self, *, delete_message_seconds: int | None = None, - delete_message_days: Literal[0, 1, 2, 3, 4, 5, 6, 7] | None = None, reason: str | None = None, ) -> None: """|coro| @@ -695,7 +694,6 @@ async def ban( self, reason=reason, delete_message_seconds=delete_message_seconds, - delete_message_days=delete_message_days, ) async def unban(self, *, reason: str | None = None) -> None: diff --git a/discord/types/guild.py b/discord/types/guild.py index be417a726d..cac645b272 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -185,3 +185,8 @@ class RolePositionUpdate(TypedDict, total=False): class GuildMFAModify(TypedDict): level: Literal[0, 1] + + +class GuildBulkBan(TypedDict): + banned_users: list[Snowflake] + failed_users: list[Snowflake] diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index 793c3a64e4..c907e2a1b4 100644 --- a/docs/ext/commands/commands.rst +++ b/docs/ext/commands/commands.rst @@ -573,11 +573,10 @@ When mixed with the :data:`typing.Optional` converter you can provide simple and @bot.command() async def ban(ctx, members: commands.Greedy[discord.Member], - delete_days: typing.Optional[int] = 0, *, + delete_seconds: typing.Optional[int] = 0, *, reason: str): - """Mass bans members with an optional delete_days parameter""" - for member in members: - await member.ban(delete_message_days=delete_days, reason=reason) + """Bulk bans members with an optional delete_seconds parameter""" + await ctx.guild.bulk_ban(*members, delete_message_seconds=delete_seconds, reason=reason) This command can be invoked any of the following ways: @@ -707,7 +706,7 @@ For example, augmenting the example above: @commands.command() async def ban(ctx, *, flags: BanFlags): for member in flags.members: - await member.ban(reason=flags.reason, delete_message_days=flags.days) + await member.ban(reason=flags.reason, delete_message_seconds=flags.days * 60 * 24) members = ', '.join(str(member) for member in flags.members) plural = f'{flags.days} days' if flags.days != 1 else f'{flags.days} day' From 1f7225fca4b72a1d9c9ebe793e876cb8af089f20 Mon Sep 17 00:00:00 2001 From: YoggieS <61660055+yoggys@users.noreply.github.com> Date: Sat, 27 Apr 2024 01:01:30 +0200 Subject: [PATCH 065/108] fix: type-hinting in Guild.bulk_ban (#2440) --- discord/guild.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index f015048f08..83758b0ad9 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3134,10 +3134,10 @@ async def bulk_ban( Example Usage: :: # Ban multiple users - successes, failures = await guild.ban(user1, user2, user3, ..., reason="Raid") + successes, failures = await guild.bulk_ban(user1, user2, user3, ..., reason="Raid") # Ban a list of users - successes, failures = await guild.ban(*users) + successes, failures = await guild.bulk_ban(*users) Parameters ---------- From 3734bb4a43ed4c14b737e75bbd17d211c9c91910 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 18:19:10 -0500 Subject: [PATCH 066/108] chore(deps-dev): update pytest requirement from ~=8.1.1 to ~=8.1.2 (#2439) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.1.1...8.1.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 476607969f..c53b5e97fb 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ -r _.txt pylint~=3.1.0 -pytest~=8.1.1 +pytest~=8.1.2 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 mypy~=1.10.0 From c3a5f1eb7ec381fa391af6f79930fd19a9d58f75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 01:24:00 +0000 Subject: [PATCH 067/108] chore(deps-dev): update pytest requirement from ~=8.1.2 to ~=8.2.0 (#2442) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.1.2...8.2.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index c53b5e97fb..143b73e75e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ -r _.txt pylint~=3.1.0 -pytest~=8.1.2 +pytest~=8.2.0 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 mypy~=1.10.0 From 2cc55028d8634840b49d9ce6d7eae0f93970c603 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 05:00:12 +0000 Subject: [PATCH 068/108] chore(pre-commit): pre-commit autoupdate (#2441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.4.0 → 24.4.2](https://github.com/psf/black/compare/24.4.0...24.4.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9719862ee2..f07b60afdb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 24.4.0 + rev: 24.4.2 hooks: - id: black args: [--safe, --quiet] From 08370b57368e2e5ce0e4e70faf8514742f45f5e5 Mon Sep 17 00:00:00 2001 From: Readeem <99687127+Readeem@users.noreply.github.com> Date: Tue, 7 May 2024 18:14:12 +0300 Subject: [PATCH 069/108] fix: AttributeError using PartialMessage.edit(embed=None) (#2446) * Update message.py * changelog --------- Co-authored-by: Readeem --- CHANGELOG.md | 2 ++ discord/message.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39df9290f2..95409d94f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2411](https://github.com/Pycord-Development/pycord/pull/2411)) - Fixed option typehints being ignored when using `parameter_name`. ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) +- Fixed parameter `embed=None` causing `AttributeError` on `PartialMessage.edit`. + ([#2446](https://github.com/Pycord-Development/pycord/pull/2446)) ### Changed diff --git a/discord/message.py b/discord/message.py index 5f4d3bc994..701bcc5ee5 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2039,7 +2039,7 @@ async def edit(self, **fields: Any) -> Message | None: raise InvalidArgument("Cannot pass both embed and embeds parameters.") if embed is not MISSING: - fields["embeds"] = [embed.to_dict()] + fields["embeds"] = [] if embed is None else [embed.to_dict()] if embeds is not MISSING: fields["embeds"] = [embed.to_dict() for embed in embeds] From 6c990b95b619bd978dff2c9ed1de2052ddd17125 Mon Sep 17 00:00:00 2001 From: BobDotCom <71356958+BobDotCom@users.noreply.github.com> Date: Fri, 10 May 2024 16:50:53 -0500 Subject: [PATCH 070/108] fix(ext-pages): revert paginator on callback fail (#2448) * fix(ext-pages): revert paginator on callback fail * style(pre-commit): auto fixes from pre-commit.com hooks * docs(changelog): update changelog --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 + discord/ext/pages/pagination.py | 93 ++++++++++++++++++--------------- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95409d94f7..cbfd7ccafa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) - Fixed parameter `embed=None` causing `AttributeError` on `PartialMessage.edit`. ([#2446](https://github.com/Pycord-Development/pycord/pull/2446)) +- Fixed paginator to revert state if a page update callback fails. + ([#2448](https://github.com/Pycord-Development/pycord/pull/2448)) ### Changed diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py index e3eca0cf4d..d3f2d32800 100644 --- a/discord/ext/pages/pagination.py +++ b/discord/ext/pages/pagination.py @@ -27,8 +27,10 @@ from typing import List import discord +from discord.errors import DiscordException from discord.ext.bridge import BridgeContext from discord.ext.commands import Context +from discord.file import File from discord.member import Member from discord.user import User @@ -103,26 +105,25 @@ async def callback(self, interaction: discord.Interaction): interaction: :class:`discord.Interaction` The interaction created by clicking the navigation button. """ + new_page = self.paginator.current_page if self.button_type == "first": - self.paginator.current_page = 0 + new_page = 0 elif self.button_type == "prev": if self.paginator.loop_pages and self.paginator.current_page == 0: - self.paginator.current_page = self.paginator.page_count + new_page = self.paginator.page_count else: - self.paginator.current_page -= 1 + new_page -= 1 elif self.button_type == "next": if ( self.paginator.loop_pages and self.paginator.current_page == self.paginator.page_count ): - self.paginator.current_page = 0 + new_page = 0 else: - self.paginator.current_page += 1 + new_page += 1 elif self.button_type == "last": - self.paginator.current_page = self.paginator.page_count - await self.paginator.goto_page( - page_number=self.paginator.current_page, interaction=interaction - ) + new_page = self.paginator.page_count + await self.paginator.goto_page(page_number=new_page, interaction=interaction) class Page: @@ -656,6 +657,20 @@ async def cancel( else: await self.message.edit(view=self) + def _goto_page(self, page_number: int = 0) -> tuple[Page, list[File] | None]: + self.current_page = page_number + self.update_buttons() + + page = self.pages[page_number] + page = self.get_page_content(page) + + if page.custom_view: + self.update_custom_view(page.custom_view) + + files = page.update_files() + + return page, files + async def goto_page( self, page_number: int = 0, *, interaction: discord.Interaction | None = None ) -> None: @@ -680,42 +695,34 @@ async def goto_page( :class:`~discord.Message` The message associated with the paginator. """ - self.update_buttons() - self.current_page = page_number - if self.show_indicator: - try: - self.buttons["page_indicator"][ - "object" - ].label = f"{self.current_page + 1}/{self.page_count + 1}" - except KeyError: - pass - - page = self.pages[page_number] - page = self.get_page_content(page) + old_page = self.current_page + page, files = self._goto_page(page_number) - if page.custom_view: - self.update_custom_view(page.custom_view) - - files = page.update_files() + try: + if interaction: + await interaction.response.defer() # needed to force webhook message edit route for files kwarg support + await interaction.followup.edit_message( + message_id=self.message.id, + content=page.content, + embeds=page.embeds, + attachments=[], + files=files or [], + view=self, + ) + else: + await self.message.edit( + content=page.content, + embeds=page.embeds, + attachments=[], + files=files or [], + view=self, + ) + except DiscordException: + # Something went wrong, and the paginator couldn't be updated. + # Revert our changes and propagate the error. + self._goto_page(old_page) + raise - if interaction: - await interaction.response.defer() # needed to force webhook message edit route for files kwarg support - await interaction.followup.edit_message( - message_id=self.message.id, - content=page.content, - embeds=page.embeds, - attachments=[], - files=files or [], - view=self, - ) - else: - await self.message.edit( - content=page.content, - embeds=page.embeds, - attachments=[], - files=files or [], - view=self, - ) if self.trigger_on_display: await self.page_action(interaction=interaction) From ea8a6fe5561fc2be326eabb773ce0f8d7f18eb90 Mon Sep 17 00:00:00 2001 From: BobDotCom <71356958+BobDotCom@users.noreply.github.com> Date: Mon, 13 May 2024 16:13:17 -0500 Subject: [PATCH 071/108] fix: remove module-level deprecation warn (#2450) * fix: remove module-level deprecation warn * feat: add stacklevel kwarg to depecation utils * fix: rewrite warning logic into cmd init * docs(changelog): update changelog --- CHANGELOG.md | 2 ++ discord/ext/bridge/core.py | 32 ++++++++++++++++++++++---------- discord/utils.py | 8 +++++++- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbfd7ccafa..041460ef53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2421](https://github.com/Pycord-Development/pycord/pull/2421)) - Added `member` data to the `raw_reaction_remove` event. ([#2412](https://github.com/Pycord-Development/pycord/pull/2412)) +- Added `stacklevel` param to `utils.warn_deprecated` and `utils.deprecated`. + ([#2450](https://github.com/Pycord-Development/pycord/pull/2450)) ### Fixed diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py index c14d9db95a..384e9ab37a 100644 --- a/discord/ext/bridge/core.py +++ b/discord/ext/bridge/core.py @@ -96,6 +96,28 @@ class BridgeExtCommand(Command): def __init__(self, func, **kwargs): super().__init__(func, **kwargs) + # TODO: v2.7: Remove backwards support for Option in bridge commands. + for name, option in self.params.items(): + if isinstance(option.annotation, Option) and not isinstance( + option.annotation, BridgeOption + ): + # Warn not to do this + warn_deprecated( + "Using Option for bridge commands", + "BridgeOption", + "2.5", + "2.7", + reference="https://github.com/Pycord-Development/pycord/pull/2417", + stacklevel=6, + ) + # Override the convert method of the parameter's annotated Option. + # We can use the convert method from BridgeOption, and bind "self" + # using a manual invocation of the descriptor protocol. + # Definitely not a good approach, but gets the job done until removal. + self.params[name].annotation.convert = BridgeOption.convert.__get__( + self.params[name].annotation + ) + async def dispatch_error(self, ctx: BridgeExtContext, error: Exception) -> None: await super().dispatch_error(ctx, error) ctx.bot.dispatch("bridge_command_error", ctx, error) @@ -653,13 +675,3 @@ def decorator(func): return func return decorator - - -discord.commands.options.Option = BridgeOption -discord.Option = BridgeOption -warn_deprecated( - "Option", - "BridgeOption", - "2.5", - reference="https://github.com/Pycord-Development/pycord/pull/2417", -) diff --git a/discord/utils.py b/discord/utils.py index 9d982a3aef..b95678a1c4 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -296,6 +296,7 @@ def warn_deprecated( since: str | None = None, removed: str | None = None, reference: str | None = None, + stacklevel: int = 3, ) -> None: """Warn about a deprecated function, with the ability to specify details about the deprecation. Emits a DeprecationWarning. @@ -315,6 +316,8 @@ def warn_deprecated( reference: Optional[:class:`str`] A reference that explains the deprecation, typically a URL to a page such as a changelog entry or a GitHub issue/PR. + stacklevel: :class:`int` + The stacklevel kwarg passed to :func:`warnings.warn`. Defaults to 3. """ warnings.simplefilter("always", DeprecationWarning) # turn off filter message = f"{name} is deprecated" @@ -328,7 +331,7 @@ def warn_deprecated( if reference: message += f" See {reference} for more information." - warnings.warn(message, stacklevel=3, category=DeprecationWarning) + warnings.warn(message, stacklevel=stacklevel, category=DeprecationWarning) warnings.simplefilter("default", DeprecationWarning) # reset filter @@ -337,6 +340,7 @@ def deprecated( since: str | None = None, removed: str | None = None, reference: str | None = None, + stacklevel: int = 3, *, use_qualname: bool = True, ) -> Callable[[Callable[[P], T]], Callable[[P], T]]: @@ -356,6 +360,8 @@ def deprecated( reference: Optional[:class:`str`] A reference that explains the deprecation, typically a URL to a page such as a changelog entry or a GitHub issue/PR. + stacklevel: :class:`int` + The stacklevel kwarg passed to :func:`warnings.warn`. Defaults to 3. use_qualname: :class:`bool` Whether to use the qualified name of the function in the deprecation warning. If ``False``, the short name of the function will be used instead. For example, __qualname__ will display as ``Client.login`` while __name__ From f78caa49a06f6f1af7ced2726b92652b7371913a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 23:28:20 +0000 Subject: [PATCH 072/108] chore(deps-dev): update pylint requirement from ~=3.1.0 to ~=3.1.1 (#2452) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.1.0...v3.1.1) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 143b73e75e..a0148f3f4e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r _.txt -pylint~=3.1.0 +pylint~=3.1.1 pytest~=8.2.0 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 From e7e014601426b4564713fd275a430e0c77417340 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 12:18:32 +0000 Subject: [PATCH 073/108] chore(deps-dev): update pylint requirement from ~=3.1.1 to ~=3.2.0 (#2453) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.1.1...v3.2.0) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index a0148f3f4e..baad0b744f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r _.txt -pylint~=3.1.1 +pylint~=3.2.0 pytest~=8.2.0 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 From d0b9f5ef7861d2b23f174aa10cbea866690b4bf4 Mon Sep 17 00:00:00 2001 From: YoggieS <61660055+yoggys@users.noreply.github.com> Date: Sun, 19 May 2024 18:38:20 +0200 Subject: [PATCH 074/108] fix: missing `application_id` in `Entitlement.delete` (#2458) * fix: missing `application_id` in `Entitlement.delete` * add changelog entry --- CHANGELOG.md | 2 ++ discord/monetization.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 041460ef53..9300e62a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2446](https://github.com/Pycord-Development/pycord/pull/2446)) - Fixed paginator to revert state if a page update callback fails. ([#2448](https://github.com/Pycord-Development/pycord/pull/2448)) +- Fixed missing `application_id` in `Entitlement.delete`. + ([#2458](https://github.com/Pycord-Development/pycord/pull/2458)) ### Changed diff --git a/discord/monetization.py b/discord/monetization.py index 910ad90916..487c6659e1 100644 --- a/discord/monetization.py +++ b/discord/monetization.py @@ -173,4 +173,4 @@ async def delete(self) -> None: HTTPException Deleting the entitlement failed. """ - await self._state.http.delete_test_entitlement(self.id) + await self._state.http.delete_test_entitlement(self.application_id, self.id) From a173a763794b92f800f2e07d2d6b73f766bc9198 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 07:49:54 +0000 Subject: [PATCH 075/108] chore(deps-dev): update pytest requirement from ~=8.2.0 to ~=8.2.1 (#2460) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index baad0b744f..a6ada004e0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ -r _.txt pylint~=3.2.0 -pytest~=8.2.0 +pytest~=8.2.1 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 mypy~=1.10.0 From 2c4ef46ab8d0052f31a2d1014647b1f4f8cb1ec7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 07:54:20 +0000 Subject: [PATCH 076/108] chore(deps-dev): bump codespell from 2.2.6 to 2.3.0 (#2461) Bumps [codespell](https://github.com/codespell-project/codespell) from 2.2.6 to 2.3.0. - [Release notes](https://github.com/codespell-project/codespell/releases) - [Commits](https://github.com/codespell-project/codespell/compare/v2.2.6...v2.3.0) --- updated-dependencies: - dependency-name: codespell dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index a6ada004e0..1e0f5be4c0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -6,6 +6,6 @@ pytest-asyncio~=0.23.3 mypy~=1.10.0 coverage~=7.5 pre-commit==3.5.0 -codespell==2.2.6 +codespell==2.3.0 bandit==1.7.8 flake8==7.0.0 From 9fc3d7b696a586d5c3c3d341b8a625c8e2384210 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 07:58:25 +0000 Subject: [PATCH 077/108] chore(deps-dev): update pylint requirement from ~=3.2.0 to ~=3.2.2 (#2459) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 1e0f5be4c0..c9654982d7 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r _.txt -pylint~=3.2.0 +pylint~=3.2.2 pytest~=8.2.1 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 From 61cc544abdc0463c3e3fe866b12bda6a2064333c Mon Sep 17 00:00:00 2001 From: YoggieS <61660055+yoggys@users.noreply.github.com> Date: Wed, 5 Jun 2024 22:49:43 +0200 Subject: [PATCH 078/108] fix: overall type-hinting improvements (#2457) * fix: RawMemberRemoveEvent type-hinting * Update CHANGELOG.md Signed-off-by: YoggieS <61660055+yoggys@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * Update CHANGELOG.md Co-authored-by: plun1331 Signed-off-by: YoggieS <61660055+yoggys@users.noreply.github.com> * fix: overall type-hinting * style(pre-commit): auto fixes from pre-commit.com hooks * fix: further types lookup & fixes * style(pre-commit): auto fixes from pre-commit.com hooks * Update discord/guild.py Co-authored-by: UK <41271523+NeloBlivion@users.noreply.github.com> Signed-off-by: YoggieS <61660055+yoggys@users.noreply.github.com> * review suggestions * review suggestions v2 * style(pre-commit): auto fixes from pre-commit.com hooks * review suggestions v3 * update changelog with correct # * nonce typing partial revert * onboarding PromptOption types * Update CHANGELOG.md Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: YoggieS <61660055+yoggys@users.noreply.github.com> --------- Signed-off-by: YoggieS <61660055+yoggys@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 Co-authored-by: UK <41271523+NeloBlivion@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/abc.py | 12 ++++++------ discord/audit_logs.py | 10 +++++----- discord/channel.py | 2 +- discord/commands/context.py | 4 ++-- discord/commands/core.py | 6 +++--- discord/context_managers.py | 2 +- discord/ext/commands/converter.py | 4 ++-- discord/ext/commands/help.py | 8 +++++++- discord/guild.py | 10 +++++----- discord/http.py | 12 ++++++------ discord/interactions.py | 2 +- discord/invite.py | 3 ++- discord/member.py | 12 ++++++++++-- discord/message.py | 4 ++-- discord/partial_emoji.py | 4 +++- discord/raw_models.py | 2 +- discord/reaction.py | 2 +- discord/sinks/core.py | 4 +++- discord/state.py | 20 ++++++++------------ discord/types/channel.py | 4 ++-- discord/types/message.py | 4 ++++ discord/types/onboarding.py | 2 +- discord/types/webhook.py | 14 +++++++------- discord/user.py | 5 ++++- discord/voice_client.py | 4 ++-- discord/webhook/async_.py | 13 +++++++------ 27 files changed, 98 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9300e62a2f..ea7687928c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2448](https://github.com/Pycord-Development/pycord/pull/2448)) - Fixed missing `application_id` in `Entitlement.delete`. ([#2458](https://github.com/Pycord-Development/pycord/pull/2458)) +- Fixed many inaccurate type hints throughout the library. + ([#2457](https://github.com/Pycord-Development/pycord/pull/2457)) ### Changed diff --git a/discord/abc.py b/discord/abc.py index dc2237151d..270103b700 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -115,7 +115,7 @@ async def _single_delete_strategy( async def _purge_messages_helper( - channel: TextChannel | Thread | VoiceChannel, + channel: TextChannel | StageChannel | Thread | VoiceChannel, *, limit: int | None = 100, check: Callable[[Message], bool] = MISSING, @@ -1345,7 +1345,7 @@ async def send( file: File = ..., stickers: Sequence[GuildSticker | StickerItem] = ..., delete_after: float = ..., - nonce: str | int = ..., + nonce: int | str = ..., enforce_nonce: bool = ..., allowed_mentions: AllowedMentions = ..., reference: Message | MessageReference | PartialMessage = ..., @@ -1365,7 +1365,7 @@ async def send( files: list[File] = ..., stickers: Sequence[GuildSticker | StickerItem] = ..., delete_after: float = ..., - nonce: str | int = ..., + nonce: int | str = ..., enforce_nonce: bool = ..., allowed_mentions: AllowedMentions = ..., reference: Message | MessageReference | PartialMessage = ..., @@ -1385,7 +1385,7 @@ async def send( file: File = ..., stickers: Sequence[GuildSticker | StickerItem] = ..., delete_after: float = ..., - nonce: str | int = ..., + nonce: int | str = ..., enforce_nonce: bool = ..., allowed_mentions: AllowedMentions = ..., reference: Message | MessageReference | PartialMessage = ..., @@ -1405,7 +1405,7 @@ async def send( files: list[File] = ..., stickers: Sequence[GuildSticker | StickerItem] = ..., delete_after: float = ..., - nonce: str | int = ..., + nonce: int | str = ..., enforce_nonce: bool = ..., allowed_mentions: AllowedMentions = ..., reference: Message | MessageReference | PartialMessage = ..., @@ -1465,7 +1465,7 @@ async def send( The file to upload. files: List[:class:`~discord.File`] A list of files to upload. Must be a maximum of 10. - nonce: :class:`int` + nonce: Union[:class:`str`, :class:`int`] The nonce to use for sending this message. If the message was successfully sent, then the message will have a nonce with this value. enforce_nonce: Optional[:class:`bool`] diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 297635038b..4bac078c4f 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -193,7 +193,7 @@ def _transform_type( def _transform_actions( entry: AuditLogEntry, data: list[AutoModActionPayload] | None -) -> AutoModAction | None: +) -> list[AutoModAction] | None: if data is None: return None else: @@ -201,8 +201,8 @@ def _transform_actions( def _transform_trigger_metadata( - entry: AuditLogEntry, data: list[AutoModActionPayload] | None -) -> AutoModAction | None: + entry: AuditLogEntry, data: AutoModTriggerMetadataPayload | None +) -> AutoModTriggerMetadata | None: if data is None: return None else: @@ -309,7 +309,7 @@ def __init__( "$add_allow_list", ]: self._handle_trigger_metadata( - self.before, self.after, entry, elem["new_value"], attr + self.before, self.after, entry, elem["new_value"], attr # type: ignore ) continue elif attr in [ @@ -318,7 +318,7 @@ def __init__( "$remove_allow_list", ]: self._handle_trigger_metadata( - self.after, self.before, entry, elem["new_value"], attr + self.after, self.before, entry, elem["new_value"], attr # type: ignore ) continue diff --git a/discord/channel.py b/discord/channel.py index 2586e3eed7..77c733194f 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1218,7 +1218,7 @@ async def create_thread( A list of stickers to upload. Must be a maximum of 3. delete_message_after: :class:`int` The time to wait before deleting the thread. - nonce: :class:`int` + nonce: Union[:class:`str`, :class:`int`] The nonce to use for sending this message. If the message was successfully sent, then the message will have a nonce with this value. allowed_mentions: :class:`~discord.AllowedMentions` diff --git a/discord/commands/context.py b/discord/commands/context.py index dda6efa539..27c3b0acba 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -37,7 +37,7 @@ import discord from .. import Bot from ..state import ConnectionState - from ..voice_client import VoiceProtocol + from ..voice_client import VoiceClient from .core import ApplicationCommand, Option from ..interactions import InteractionChannel @@ -211,7 +211,7 @@ def user(self) -> Member | User: author: Member | User = user @property - def voice_client(self) -> VoiceProtocol | None: + def voice_client(self) -> VoiceClient | None: """Returns the voice client associated with this context's command. Shorthand for :attr:`Interaction.guild.voice_client<~discord.Guild.voice_client>`, if applicable. """ diff --git a/discord/commands/core.py b/discord/commands/core.py index 6f34c0c9d9..26469472e8 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -328,7 +328,7 @@ def is_on_cooldown(self, ctx: ApplicationContext) -> bool: if not self._buckets.valid: return False - bucket = self._buckets.get_bucket(ctx) + bucket = self._buckets.get_bucket(ctx) # type: ignore current = utcnow().timestamp() return bucket.get_tokens(current) == 0 @@ -363,7 +363,7 @@ def get_cooldown_retry_after(self, ctx: ApplicationContext) -> float: If this is ``0.0`` then the command isn't on cooldown. """ if self._buckets.valid: - bucket = self._buckets.get_bucket(ctx) + bucket = self._buckets.get_bucket(ctx) # type: ignore current = utcnow().timestamp() return bucket.get_retry_after(current) @@ -1245,7 +1245,7 @@ def to_dict(self) -> dict: return as_dict - def add_command(self, command: SlashCommand) -> None: + def add_command(self, command: SlashCommand | SlashCommandGroup) -> None: if command.cog is None and self.cog is not None: command.cog = self.cog diff --git a/discord/context_managers.py b/discord/context_managers.py index c9d930b5e4..9f1a282b3f 100644 --- a/discord/context_managers.py +++ b/discord/context_managers.py @@ -38,7 +38,7 @@ __all__ = ("Typing",) -def _typing_done_callback(fut: asyncio.Future) -> None: +def _typing_done_callback(fut: asyncio.Task) -> None: # just retrieve any exception and call it a day try: fut.exception() diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 7a4bbabf46..3ed53fa1d0 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -1166,7 +1166,7 @@ async def _actual_conversion( async def run_converters( - ctx: Context, converter, argument: str, param: inspect.Parameter + ctx: Context, converter, argument: str | None, param: inspect.Parameter ): """|coro| @@ -1182,7 +1182,7 @@ async def run_converters( The invocation context to run the converters under. converter: Any The converter to run, this corresponds to the annotation in the function. - argument: :class:`str` + argument: Optional[:class:`str`] The argument to convert to. param: :class:`inspect.Parameter` The parameter being converted. This is mainly for error reporting. diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py index 90899ef6a8..25b44388bd 100644 --- a/discord/ext/commands/help.py +++ b/discord/ext/commands/help.py @@ -90,7 +90,13 @@ class Paginator: .. versionadded:: 1.7 """ - def __init__(self, prefix="```", suffix="```", max_size=2000, linesep="\n"): + def __init__( + self, + prefix: str | None = "```", + suffix: str | None = "```", + max_size: int = 2000, + linesep: str = "\n", + ): self.prefix = prefix self.suffix = suffix self.max_size = max_size diff --git a/discord/guild.py b/discord/guild.py index 83758b0ad9..15bf1394a3 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -107,7 +107,7 @@ from .types.member import Member as MemberPayload from .types.threads import Thread as ThreadPayload from .types.voice import GuildVoiceState - from .voice_client import VoiceProtocol + from .voice_client import VoiceClient from .webhook import Webhook VocalGuildChannel = Union[VoiceChannel, StageChannel] @@ -647,8 +647,8 @@ def me(self) -> Member: return self.get_member(self_id) # type: ignore @property - def voice_client(self) -> VoiceProtocol | None: - """Returns the :class:`VoiceProtocol` associated with this guild, if any.""" + def voice_client(self) -> VoiceClient | None: + """Returns the :class:`VoiceClient` associated with this guild, if any.""" return self._state._get_voice_client(self.id) @property @@ -3121,7 +3121,7 @@ async def bulk_ban( *users: Snowflake, delete_message_seconds: int | None = None, reason: str | None = None, - ) -> list[list[Snowflake], list[Snowflake]]: + ) -> tuple[list[Snowflake], list[Snowflake]]: r"""|coro| Bulk ban users from the guild. @@ -3152,7 +3152,7 @@ async def bulk_ban( Returns ------- - List[List[:class:`abc.Snowflake`], List[:class:`abc.Snowflake`]] + Tuple[List[:class:`abc.Snowflake`], List[:class:`abc.Snowflake`]] Returns two lists: the first contains members that were successfully banned, while the second is members that could not be banned. Raises diff --git a/discord/http.py b/discord/http.py index 3622afc2bd..1132ca3338 100644 --- a/discord/http.py +++ b/discord/http.py @@ -464,7 +464,7 @@ def send_message( tts: bool = False, embed: embed.Embed | None = None, embeds: list[embed.Embed] | None = None, - nonce: str | None = None, + nonce: int | str | None = None, enforce_nonce: bool | None = None, allowed_mentions: message.AllowedMentions | None = None, message_reference: message.MessageReference | None = None, @@ -524,7 +524,7 @@ def send_multipart_helper( tts: bool = False, embed: embed.Embed | None = None, embeds: Iterable[embed.Embed | None] | None = None, - nonce: str | None = None, + nonce: int | str | None = None, enforce_nonce: bool | None = None, allowed_mentions: message.AllowedMentions | None = None, message_reference: message.MessageReference | None = None, @@ -587,7 +587,7 @@ def send_files( tts: bool = False, embed: embed.Embed | None = None, embeds: list[embed.Embed] | None = None, - nonce: str | None = None, + nonce: int | str | None = None, enforce_nonce: bool | None = None, allowed_mentions: message.AllowedMentions | None = None, message_reference: message.MessageReference | None = None, @@ -1203,7 +1203,7 @@ def start_forum_thread( files: Sequence[File] | None = None, embed: embed.Embed | None = None, embeds: list[embed.Embed] | None = None, - nonce: str | None = None, + nonce: int | str | None = None, allowed_mentions: message.AllowedMentions | None = None, stickers: list[sticker.StickerItem] | None = None, components: list[components.Component] | None = None, @@ -2151,8 +2151,8 @@ def edit_channel_permissions( self, channel_id: Snowflake, target: Snowflake, - allow: str, - deny: str, + allow: int | str, + deny: int | str, type: channel.OverwriteType, *, reason: str | None = None, diff --git a/discord/interactions.py b/discord/interactions.py index 1362acfac4..51574ce0b3 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -1221,7 +1221,7 @@ async def premium_required(self) -> Interaction: self._responded = True return self._parent - async def _locked_response(self, coro: Coroutine[Any]): + async def _locked_response(self, coro: Coroutine[Any, Any, Any]) -> None: """|coro| Wraps a response and makes sure that it's locked while executing. diff --git a/discord/invite.py b/discord/invite.py index 2560bf9996..12d1d0f28a 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -49,6 +49,7 @@ from .types.invite import GatewayInvite as GatewayInvitePayload from .types.invite import Invite as InvitePayload from .types.invite import InviteGuild as InviteGuildPayload + from .types.invite import VanityInvite as VanityInvitePayload from .types.scheduled_events import ScheduledEvent as ScheduledEventPayload from .user import User @@ -353,7 +354,7 @@ def __init__( self, *, state: ConnectionState, - data: InvitePayload, + data: InvitePayload | VanityInvitePayload, guild: PartialInviteGuild | Guild | None = None, channel: PartialInviteChannel | GuildChannel | None = None, ): diff --git a/discord/member.py b/discord/member.py index c854e12979..57c04b2936 100644 --- a/discord/member.py +++ b/discord/member.py @@ -62,6 +62,7 @@ from .types.member import MemberWithUser as MemberWithUserPayload from .types.member import UserWithMember as UserWithMemberPayload from .types.user import User as UserPayload + from .types.voice import GuildVoiceState as GuildVoiceStatePayload from .types.voice import VoiceState as VoiceStatePayload VocalGuildChannel = Union[VoiceChannel, StageChannel] @@ -125,12 +126,19 @@ class VoiceState: ) def __init__( - self, *, data: VoiceStatePayload, channel: VocalGuildChannel | None = None + self, + *, + data: VoiceStatePayload | GuildVoiceStatePayload, + channel: VocalGuildChannel | None = None, ): self.session_id: str = data.get("session_id") self._update(data, channel) - def _update(self, data: VoiceStatePayload, channel: VocalGuildChannel | None): + def _update( + self, + data: VoiceStatePayload | GuildVoiceStatePayload, + channel: VocalGuildChannel | None, + ): self.self_mute: bool = data.get("self_mute", False) self.self_deaf: bool = data.get("self_deaf", False) self.self_stream: bool = data.get("self_stream", False) diff --git a/discord/message.py b/discord/message.py index 701bcc5ee5..23307434e8 100644 --- a/discord/message.py +++ b/discord/message.py @@ -240,14 +240,14 @@ def __init__(self, *, data: AttachmentPayload, state: ConnectionState): setattr(self, attr, value) @property - def expires_at(self) -> datetime.datetime: + def expires_at(self) -> datetime.datetime | None: """This attachment URL's expiry time in UTC.""" if not self._ex: return None return datetime.datetime.utcfromtimestamp(int(self._ex, 16)) @property - def issued_at(self) -> datetime.datetime: + def issued_at(self) -> datetime.datetime | None: """The attachment URL's issue time in UTC.""" if not self._is: return None diff --git a/discord/partial_emoji.py b/discord/partial_emoji.py index 171d6390c5..89009cc741 100644 --- a/discord/partial_emoji.py +++ b/discord/partial_emoji.py @@ -100,7 +100,9 @@ class PartialEmoji(_EmojiTag, AssetMixin): if TYPE_CHECKING: id: int | None - def __init__(self, *, name: str, animated: bool = False, id: int | None = None): + def __init__( + self, *, name: str | None, animated: bool = False, id: int | None = None + ): self.animated = animated self.name = name self.id = id diff --git a/discord/raw_models.py b/discord/raw_models.py index e3d38e558d..94226ff459 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -30,7 +30,6 @@ from .automod import AutoModAction, AutoModTriggerType from .enums import AuditLogAction, ChannelType, ReactionType, try_enum -from .types.user import User if TYPE_CHECKING: from .abc import MessageableChannel @@ -58,6 +57,7 @@ TypingEvent, VoiceChannelStatusUpdateEvent, ) + from .user import User __all__ = ( diff --git a/discord/reaction.py b/discord/reaction.py index 426b5474ef..2726e8984f 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -111,7 +111,7 @@ def __init__( self.me: bool = data.get("me") self.burst: bool = data.get("burst") self.me_burst: bool = data.get("me_burst") - self._burst_colours: list[Colour] = data.get("burst_colors", []) + self._burst_colours: list[str] = data.get("burst_colors", []) @property def burst_colours(self) -> list[Colour]: diff --git a/discord/sinks/core.py b/discord/sinks/core.py index 20d6d3a9ac..c8ea31a82a 100644 --- a/discord/sinks/core.py +++ b/discord/sinks/core.py @@ -23,6 +23,8 @@ DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations + import io import os import struct @@ -202,7 +204,7 @@ def __init__(self, *, filters=None): filters = default_filters self.filters = filters Filters.__init__(self, **self.filters) - self.vc: VoiceClient = None + self.vc: VoiceClient | None = None self.audio_data = {} def init(self, vc): # called under listen diff --git a/discord/state.py b/discord/state.py index 0f9a6dc438..c3ae777f5f 100644 --- a/discord/state.py +++ b/discord/state.py @@ -43,8 +43,6 @@ Union, ) -import discord - from . import utils from .activity import BaseActivity from .audit_logs import AuditLogEntry @@ -88,7 +86,7 @@ from .types.message import Message as MessagePayload from .types.sticker import GuildSticker as GuildStickerPayload from .types.user import User as UserPayload - from .voice_client import VoiceProtocol + from .voice_client import VoiceClient T = TypeVar("T") CS = TypeVar("CS", bound="ConnectionState") @@ -146,9 +144,7 @@ def done(self) -> None: _log = logging.getLogger(__name__) -async def logging_coroutine( - coroutine: Coroutine[Any, Any, T], *, info: str -) -> T | None: +async def logging_coroutine(coroutine: Coroutine[Any, Any, T], *, info: str) -> None: try: await coroutine except Exception: @@ -281,7 +277,7 @@ def clear(self, *, views: bool = True) -> None: if views: self._view_store: ViewStore = ViewStore(self) self._modal_store: ModalStore = ModalStore(self) - self._voice_clients: dict[int, VoiceProtocol] = {} + self._voice_clients: dict[int, VoiceClient] = {} # LRU of max size 128 self._private_channels: OrderedDict[int, PrivateChannel] = OrderedDict() @@ -334,14 +330,14 @@ def intents(self) -> Intents: return ret @property - def voice_clients(self) -> list[VoiceProtocol]: + def voice_clients(self) -> list[VoiceClient]: return list(self._voice_clients.values()) - def _get_voice_client(self, guild_id: int | None) -> VoiceProtocol | None: + def _get_voice_client(self, guild_id: int | None) -> VoiceClient | None: # the keys of self._voice_clients are ints return self._voice_clients.get(guild_id) # type: ignore - def _add_voice_client(self, guild_id: int, voice: VoiceProtocol) -> None: + def _add_voice_client(self, guild_id: int, voice: VoiceClient) -> None: self._voice_clients[guild_id] = voice def _remove_voice_client(self, guild_id: int) -> None: @@ -531,9 +527,9 @@ async def chunker( async def query_members( self, guild: Guild, - query: str, + query: str | None, limit: int, - user_ids: list[int], + user_ids: list[int] | None, cache: bool, presences: bool, ): diff --git a/discord/types/channel.py b/discord/types/channel.py index 0f4c044aed..1b7fb1fe5e 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -32,7 +32,7 @@ from ..flags import ChannelFlags from .snowflake import Snowflake from .threads import ThreadArchiveDuration, ThreadMember, ThreadMetadata -from .user import PartialUser +from .user import User OverwriteType = Literal[0, 1] @@ -159,7 +159,7 @@ class DMChannel(TypedDict): id: Snowflake type: Literal[1] last_message_id: Snowflake | None - recipients: list[PartialUser] + recipients: list[User] class GroupDMChannel(_BaseChannel): diff --git a/discord/types/message.py b/discord/types/message.py index 10d819ebd4..ee453f9694 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -55,6 +55,9 @@ class Reaction(TypedDict): me: bool emoji: PartialEmoji burst: bool + me_burst: bool + burst_colors: list[str] + count_details: ReactionCountDetails class ReactionCountDetails(TypedDict): @@ -66,6 +69,7 @@ class Attachment(TypedDict): height: NotRequired[int | None] width: NotRequired[int | None] content_type: NotRequired[str] + description: NotRequired[str] spoiler: NotRequired[bool] id: Snowflake filename: str diff --git a/discord/types/onboarding.py b/discord/types/onboarding.py index 4fd4027bcc..7bfa44dc13 100644 --- a/discord/types/onboarding.py +++ b/discord/types/onboarding.py @@ -61,4 +61,4 @@ class PromptOption(TypedDict): emoji_name: NotRequired[str] emoji_animated: NotRequired[bool] title: str - description: NotRequired[str] + description: str | None diff --git a/discord/types/webhook.py b/discord/types/webhook.py index b5db096e11..20fa0e900e 100644 --- a/discord/types/webhook.py +++ b/discord/types/webhook.py @@ -42,13 +42,6 @@ class SourceGuild(TypedDict): WebhookType = Literal[1, 2, 3] -class FollowerWebhook(TypedDict): - source_channel: NotRequired[PartialChannel] - source_guild: NotRequired[SourceGuild] - channel_id: Snowflake - webhook_id: Snowflake - - class PartialWebhook(TypedDict): guild_id: NotRequired[Snowflake] user: NotRequired[User] @@ -57,6 +50,13 @@ class PartialWebhook(TypedDict): type: WebhookType +class FollowerWebhook(PartialWebhook): + source_channel: NotRequired[PartialChannel] + source_guild: NotRequired[SourceGuild] + channel_id: Snowflake + webhook_id: Snowflake + + class Webhook(PartialWebhook): name: NotRequired[str | None] avatar: NotRequired[str | None] diff --git a/discord/user.py b/discord/user.py index 5cd0352df3..57580575a0 100644 --- a/discord/user.py +++ b/discord/user.py @@ -43,6 +43,7 @@ from .message import Message from .state import ConnectionState from .types.channel import DMChannel as DMChannelPayload + from .types.user import PartialUser as PartialUserPayload from .types.user import User as UserPayload @@ -89,7 +90,9 @@ class BaseUser(_UserTag): _avatar_decoration: dict | None _public_flags: int - def __init__(self, *, state: ConnectionState, data: UserPayload) -> None: + def __init__( + self, *, state: ConnectionState, data: UserPayload | PartialUserPayload + ) -> None: self._state = state self._update(data) diff --git a/discord/voice_client.py b/discord/voice_client.py index 46fc293e12..b9d4766883 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -532,14 +532,14 @@ async def disconnect(self, *, force: bool = False) -> None: if self.socket: self.socket.close() - async def move_to(self, channel: abc.Snowflake) -> None: + async def move_to(self, channel: abc.Connectable) -> None: """|coro| Moves you to a different voice channel. Parameters ---------- - channel: :class:`abc.Snowflake` + channel: :class:`abc.Connectable` The channel to move to. Must be a voice channel. """ await self.channel.guild.change_voice_state(channel=channel) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index d14f9e6c5d..37e8d4c894 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -76,6 +76,7 @@ from ..mentions import AllowedMentions from ..state import ConnectionState from ..types.message import Message as MessagePayload + from ..types.webhook import FollowerWebhook as FollowerWebhookPayload from ..types.webhook import Webhook as WebhookPayload from ..ui.view import View @@ -401,7 +402,7 @@ def edit_webhook_message( payload: dict[str, Any] | None = None, multipart: list[dict[str, Any]] | None = None, files: list[File] | None = None, - ) -> Response[Message]: + ) -> Response[WebhookMessage]: params = {} if thread_id: @@ -460,7 +461,7 @@ def fetch_webhook( session: aiohttp.ClientSession, proxy: str | None = None, proxy_auth: aiohttp.BasicAuth | None = None, - ) -> Response[WebhookPayload]: + ) -> Response[WebhookPayload | FollowerWebhookPayload]: route = Route("GET", "/webhooks/{webhook_id}", webhook_id=webhook_id) return self.request( route, session=session, proxy=proxy, proxy_auth=proxy_auth, auth_token=token @@ -474,7 +475,7 @@ def fetch_webhook_with_token( session: aiohttp.ClientSession, proxy: str | None = None, proxy_auth: aiohttp.BasicAuth | None = None, - ) -> Response[WebhookPayload]: + ) -> Response[WebhookPayload | FollowerWebhookPayload]: route = Route( "GET", "/webhooks/{webhook_id}/{webhook_token}", @@ -985,7 +986,7 @@ class BaseWebhook(Hashable): def __init__( self, - data: WebhookPayload, + data: WebhookPayload | FollowerWebhookPayload, token: str | None = None, state: ConnectionState | None = None, ): @@ -995,7 +996,7 @@ def __init__( ) self._update(data) - def _update(self, data: WebhookPayload): + def _update(self, data: WebhookPayload | FollowerWebhookPayload): self.id = int(data["id"]) self.type = try_enum(WebhookType, int(data["type"])) self.channel_id = utils._get_as_snowflake(data, "channel_id") @@ -1154,7 +1155,7 @@ async def foo(): def __init__( self, - data: WebhookPayload, + data: WebhookPayload | FollowerWebhookPayload, session: aiohttp.ClientSession, proxy: str | None = None, proxy_auth: aiohttp.BasicAuth | None = None, From e33fdb956cc52e2a843dbada245c83591d241031 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:22:28 +0000 Subject: [PATCH 079/108] chore(deps-dev): update pylint requirement from ~=3.2.2 to ~=3.2.3 (#2470) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.2...v3.2.3) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index c9654982d7..2278b61953 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r _.txt -pylint~=3.2.2 +pylint~=3.2.3 pytest~=8.2.1 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 From 540e064e4dc1e5d55b82cc80f64c573ff1daca77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:28:36 +0000 Subject: [PATCH 080/108] chore(deps-dev): update pytest requirement from ~=8.2.1 to ~=8.2.2 (#2469) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.1...8.2.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 2278b61953..5dcdd1809c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ -r _.txt pylint~=3.2.3 -pytest~=8.2.1 +pytest~=8.2.2 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 mypy~=1.10.0 From f015bc477d7ef03e836167bcf345b562b4c08cc6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 10:17:34 +0200 Subject: [PATCH 081/108] chore(pre-commit): pre-commit autoupdate (#2471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.2 → v3.16.0](https://github.com/asottile/pyupgrade/compare/v3.15.2...v3.16.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f07b60afdb..ab0e6556f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: # - --remove-duplicate-keys # - --remove-unused-variables - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.16.0 hooks: - id: pyupgrade args: [--py38-plus] From 58a329f92c8641c78d501346f20ff5570f97116d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 08:23:15 +0000 Subject: [PATCH 082/108] chore(deps-dev): bump bandit from 1.7.8 to 1.7.9 (#2472) Bumps [bandit](https://github.com/PyCQA/bandit) from 1.7.8 to 1.7.9. - [Release notes](https://github.com/PyCQA/bandit/releases) - [Commits](https://github.com/PyCQA/bandit/compare/1.7.8...1.7.9) --- updated-dependencies: - dependency-name: bandit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 5dcdd1809c..c523385729 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -7,5 +7,5 @@ mypy~=1.10.0 coverage~=7.5 pre-commit==3.5.0 codespell==2.3.0 -bandit==1.7.8 +bandit==1.7.9 flake8==7.0.0 From cd9ca500e229f004c1bd51b2725abd7b23f5af2c Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 15 Jun 2024 15:58:39 +0100 Subject: [PATCH 083/108] feat: Polls (#2408) Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> Signed-off-by: Lala Sabathil Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: Lala Sabathil --- CHANGELOG.md | 2 + discord/__init__.py | 1 + discord/abc.py | 16 ++ discord/client.py | 24 ++ discord/enums.py | 6 + discord/flags.py | 68 +++++ discord/http.py | 47 ++++ discord/interactions.py | 9 + discord/iterators.py | 55 ++++ discord/message.py | 82 ++++++ discord/permissions.py | 9 + discord/poll.py | 522 ++++++++++++++++++++++++++++++++++++ discord/raw_models.py | 41 +++ discord/state.py | 74 +++++ discord/types/message.py | 2 + discord/types/poll.py | 63 +++++ discord/types/raw_models.py | 8 + discord/webhook/async_.py | 15 ++ docs/api/data_classes.rst | 28 ++ docs/api/enums.rst | 11 + docs/api/events.rst | 53 ++++ 21 files changed, 1136 insertions(+) create mode 100644 discord/poll.py create mode 100644 discord/types/poll.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7687928c..aabbc5b987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2421](https://github.com/Pycord-Development/pycord/pull/2421)) - Added `member` data to the `raw_reaction_remove` event. ([#2412](https://github.com/Pycord-Development/pycord/pull/2412)) +- Added `Poll` and all related features. + ([#2408](https://github.com/Pycord-Development/pycord/pull/2408)) - Added `stacklevel` param to `utils.warn_deprecated` and `utils.deprecated`. ([#2450](https://github.com/Pycord-Development/pycord/pull/2450)) diff --git a/discord/__init__.py b/discord/__init__.py index 5564eebe77..d6031ce3ac 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -58,6 +58,7 @@ from .partial_emoji import * from .permissions import * from .player import * +from .poll import * from .raw_models import * from .reaction import * from .role import * diff --git a/discord/abc.py b/discord/abc.py index 270103b700..d699f44702 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -89,6 +89,7 @@ from .guild import Guild from .member import Member from .message import Message, MessageReference, PartialMessage + from .poll import Poll from .state import ConnectionState from .threads import Thread from .types.channel import Channel as ChannelPayload @@ -1351,6 +1352,7 @@ async def send( reference: Message | MessageReference | PartialMessage = ..., mention_author: bool = ..., view: View = ..., + poll: Poll = ..., suppress: bool = ..., silent: bool = ..., ) -> Message: ... @@ -1371,6 +1373,7 @@ async def send( reference: Message | MessageReference | PartialMessage = ..., mention_author: bool = ..., view: View = ..., + poll: Poll = ..., suppress: bool = ..., silent: bool = ..., ) -> Message: ... @@ -1391,6 +1394,7 @@ async def send( reference: Message | MessageReference | PartialMessage = ..., mention_author: bool = ..., view: View = ..., + poll: Poll = ..., suppress: bool = ..., silent: bool = ..., ) -> Message: ... @@ -1411,6 +1415,7 @@ async def send( reference: Message | MessageReference | PartialMessage = ..., mention_author: bool = ..., view: View = ..., + poll: Poll = ..., suppress: bool = ..., silent: bool = ..., ) -> Message: ... @@ -1432,6 +1437,7 @@ async def send( reference=None, mention_author=None, view=None, + poll=None, suppress=None, silent=None, ): @@ -1515,6 +1521,10 @@ async def send( Whether to suppress push and desktop notifications for the message. .. versionadded:: 2.4 + poll: :class:`Poll` + The poll to send. + + .. versionadded:: 2.6 Returns ------- @@ -1594,6 +1604,9 @@ async def send( else: components = None + if poll: + poll = poll.to_dict() + if file is not None and files is not None: raise InvalidArgument("cannot pass both file and files parameter to send()") @@ -1616,6 +1629,7 @@ async def send( stickers=stickers, components=components, flags=flags, + poll=poll, ) finally: file.close() @@ -1643,6 +1657,7 @@ async def send( stickers=stickers, components=components, flags=flags, + poll=poll, ) finally: for f in files: @@ -1661,6 +1676,7 @@ async def send( stickers=stickers, components=components, flags=flags, + poll=poll, ) ret = state.create_message(channel=channel, data=data) diff --git a/discord/client.py b/discord/client.py index 5d120f38d5..d46964042f 100644 --- a/discord/client.py +++ b/discord/client.py @@ -70,6 +70,7 @@ from .channel import DMChannel from .member import Member from .message import Message + from .poll import Poll from .voice_client import VoiceProtocol __all__ = ("Client",) @@ -338,6 +339,14 @@ def stickers(self) -> list[GuildSticker]: """ return self._connection.stickers + @property + def polls(self) -> list[Poll]: + """The polls that the connected client has. + + .. versionadded:: 2.6 + """ + return self._connection.polls + @property def cached_messages(self) -> Sequence[Message]: """Read-only list of messages the connected client has cached. @@ -1010,6 +1019,21 @@ def get_sticker(self, id: int, /) -> GuildSticker | None: """ return self._connection.get_sticker(id) + def get_poll(self, id: int, /) -> Poll | None: + """Returns a poll attached to the given message ID. + + Parameters + ---------- + id: :class:`int` + The message ID of the poll to search for. + + Returns + ------- + Optional[:class:`.Poll`] + The poll or ``None`` if not found. + """ + return self._connection.get_poll(id) + def get_all_channels(self) -> Generator[GuildChannel, None, None]: """A generator that retrieves every :class:`.abc.GuildChannel` the client can 'access'. diff --git a/discord/enums.py b/discord/enums.py index 98e46e646d..be7510fffc 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1020,6 +1020,12 @@ class EntitlementOwnerType(Enum): user = 2 +class PollLayoutType(Enum): + """The poll's layout type.""" + + default = 1 + + T = TypeVar("T") diff --git a/discord/flags.py b/discord/flags.py index 3201e5be89..37fd8baa24 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -885,6 +885,8 @@ def messages(self): - :class:`Message` - :attr:`Client.cached_messages` - :meth:`Client.get_message` + - :attr:`Client.polls` + - :meth:`Client.get_poll` Note that due to an implicit relationship this also corresponds to the following events: @@ -917,6 +919,8 @@ def guild_messages(self): - :class:`Message` - :attr:`Client.cached_messages` (only for guilds) - :meth:`Client.get_message` (only for guilds) + - :attr:`Client.polls` (only for guilds) + - :meth:`Client.get_poll` (only for guilds) Note that due to an implicit relationship this also corresponds to the following events: @@ -931,6 +935,7 @@ def guild_messages(self): - :attr:`Message.embeds` - :attr:`Message.attachments` - :attr:`Message.components` + - :attr:`Message.poll` For more information go to the :ref:`message content intent documentation `. """ @@ -955,6 +960,8 @@ def dm_messages(self): - :class:`Message` - :attr:`Client.cached_messages` (only for DMs) - :meth:`Client.get_message` (only for DMs) + - :attr:`Client.polls` (only for DMs) + - :meth:`Client.get_poll` (only for DMs) Note that due to an implicit relationship this also corresponds to the following events: @@ -1079,6 +1086,7 @@ def message_content(self): - :attr:`Message.embeds` - :attr:`Message.attachments` - :attr:`Message.components` + - :attr:`Message.poll` These attributes will still be available for messages received from interactions, the bot's own messages, messages the bot was mentioned in, and DMs. @@ -1137,6 +1145,66 @@ def auto_moderation_execution(self): """ return 1 << 21 + @flag_value + def guild_polls(self): + """:class:`bool`: Whether poll-related events in guilds are enabled. + + See also :attr:`dm_polls` for DMs or :attr:`polls` for both. + + This corresponds to the following events: + + - :func:`on_poll_vote_add` (only for guilds) + - :func:`on_poll_vote_remove` (only for guilds) + - :func:`on_raw_poll_vote_add` (only for guilds) + - :func:`on_raw_poll_vote_remove` (only for guilds) + + This also corresponds to the following attributes and classes in terms of cache: + + - :attr:`PollAnswer.count` (only for guild polls) + - :attr:`PollResults.answer_counts` (only for guild polls) + """ + return 1 << 24 + + @flag_value + def dm_polls(self): + """:class:`bool`: Whether poll-related events in direct messages are enabled. + + See also :attr:`guild_polls` for guilds or :attr:`polls` for both. + + This corresponds to the following events: + + - :func:`on_poll_vote_add` (only for DMs) + - :func:`on_poll_vote_remove` (only for DMs) + - :func:`on_raw_poll_vote_add` (only for DMs) + - :func:`on_raw_poll_vote_remove` (only for DMs) + + This also corresponds to the following attributes and classes in terms of cache: + + - :attr:`PollAnswer.count` (only for DM polls) + - :attr:`PollResults.answer_counts` (only for DM polls) + """ + return 1 << 25 + + @alias_flag_value + def polls(self): + """:class:`bool`: Whether poll-related events in guilds and direct messages are enabled. + + This is a shortcut to set or get both :attr:`guild_polls` and :attr:`dm_polls`. + + This corresponds to the following events: + + - :func:`on_poll_vote_add` (both guilds and DMs) + - :func:`on_poll_vote_remove` (both guilds and DMs) + - :func:`on_raw_poll_vote_add` (both guilds and DMs) + - :func:`on_raw_poll_vote_remove` (both guilds and DMs) + + This also corresponds to the following attributes and classes in terms of cache: + + - :attr:`PollAnswer.count` (both guild and DM polls) + - :attr:`PollResults.answer_counts` (both guild and DM polls) + """ + return (1 << 24) | (1 << 25) + @fill_with_flags() class MemberCacheFlags(BaseFlags): diff --git a/discord/http.py b/discord/http.py index 1132ca3338..85ea7c002c 100644 --- a/discord/http.py +++ b/discord/http.py @@ -71,6 +71,7 @@ message, monetization, onboarding, + poll, role, scheduled_events, sticker, @@ -471,6 +472,7 @@ def send_message( stickers: list[sticker.StickerItem] | None = None, components: list[components.Component] | None = None, flags: int | None = None, + poll: poll.Poll | None = None, ) -> Response[message.Message]: r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id) payload = {} @@ -508,6 +510,9 @@ def send_message( if flags: payload["flags"] = flags + if poll: + payload["poll"] = poll + return self.request(r, json=payload) def send_typing(self, channel_id: Snowflake) -> Response[None]: @@ -531,6 +536,7 @@ def send_multipart_helper( stickers: list[sticker.StickerItem] | None = None, components: list[components.Component] | None = None, flags: int | None = None, + poll: poll.Poll | None = None, ) -> Response[message.Message]: form = [] @@ -555,6 +561,8 @@ def send_multipart_helper( payload["sticker_ids"] = stickers if flags: payload["flags"] = flags + if poll: + payload["poll"] = poll attachments = [] form.append({"name": "payload_json"}) @@ -594,6 +602,7 @@ def send_files( stickers: list[sticker.StickerItem] | None = None, components: list[components.Component] | None = None, flags: int | None = None, + poll: poll.Poll | None = None, ) -> Response[message.Message]: r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id) return self.send_multipart_helper( @@ -610,6 +619,7 @@ def send_files( stickers=stickers, components=components, flags=flags, + poll=poll, ) def edit_multipart_helper( @@ -3003,6 +3013,43 @@ def edit_onboarding( reason=reason, ) + # Polls + + def expire_poll( + self, channel_id: Snowflake, message_id: Snowflake + ) -> Response[message.Message]: + return self.request( + Route( + "POST", + "/channels/{channel_id}/polls/{message_id}/expire", + channel_id=channel_id, + message_id=message_id, + ) + ) + + def get_answer_voters( + self, + channel_id: Snowflake, + message_id: Snowflake, + answer_id: int, + limit: int, + after: Snowflake | None = None, + ) -> Response[list[user.User]]: + r = Route( + "GET", + "/channels/{channel_id}/polls/{message_id}/answers/{answer_id}", + channel_id=channel_id, + message_id=message_id, + answer_id=answer_id, + ) + + params: dict[str, Any] = { + "limit": limit, + } + if after: + params["after"] = after + return self.request(r, params=params) + # Misc def application_info(self) -> Response[appinfo.AppInfo]: diff --git a/discord/interactions.py b/discord/interactions.py index 51574ce0b3..b2663e0b1d 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -71,6 +71,7 @@ from .commands import OptionChoice from .embeds import Embed from .mentions import AllowedMentions + from .poll import Poll from .state import ConnectionState from .threads import Thread from .types.interactions import Interaction as InteractionPayload @@ -803,6 +804,7 @@ async def send_message( allowed_mentions: AllowedMentions = None, file: File = None, files: list[File] = None, + poll: Poll = None, delete_after: float = None, ) -> Interaction: """|coro| @@ -837,6 +839,10 @@ async def send_message( The file to upload. files: List[:class:`File`] A list of files to upload. Must be a maximum of 10. + poll: :class:`Poll` + The poll to send. + + .. versionadded:: 2.6 Returns ------- @@ -881,6 +887,9 @@ async def send_message( if view is not None: payload["components"] = view.to_components() + if poll is not None: + payload["poll"] = poll.to_dict() + state = self._parent._state if allowed_mentions is None: diff --git a/discord/iterators.py b/discord/iterators.py index 1537dc8605..438c41d2ef 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -237,6 +237,61 @@ async def fill_users(self): await self.users.put(User(state=self.state, data=element)) +class VoteIterator(_AsyncIterator[Union["User", "Member"]]): + def __init__(self, message, answer, limit=100, after=None): + self.message = message + self.limit = limit + self.after = after + state = message._state + self.getter = state.http.get_answer_voters + self.state = state + self.answer = answer + self.guild = message.guild + self.channel_id = message.channel.id + self.users = asyncio.Queue() + + async def next(self) -> User | Member: + if self.users.empty(): + await self.fill_users() + + try: + return self.users.get_nowait() + except asyncio.QueueEmpty: + raise NoMoreItems() + + async def fill_users(self): + # import here to prevent circular imports + from .user import User + + if self.limit > 0: + retrieve = min(self.limit, 100) + + after = self.after.id if self.after else None + req = await self.getter( + self.channel_id, + self.message.id, + self.answer.id, + retrieve, + after=after, + ) + data: list[PartialUserPayload] = req.get("users", []) + + if data: + self.limit -= retrieve + self.after = Object(id=int(data[-1]["id"])) + + for element in reversed(data): + if self.guild is None or isinstance(self.guild, Object): + await self.users.put(User(state=self.state, data=element)) + else: + member_id = int(element["id"]) + member = self.guild.get_member(member_id) + if member is not None: + await self.users.put(member) + else: + await self.users.put(User(state=self.state, data=element)) + + class HistoryIterator(_AsyncIterator["Message"]): """Iterator for receiving a channel's message history. diff --git a/discord/message.py b/discord/message.py index 23307434e8..b97d8cd86c 100644 --- a/discord/message.py +++ b/discord/message.py @@ -53,6 +53,7 @@ from .member import Member from .mixins import Hashable from .partial_emoji import PartialEmoji +from .poll import Poll from .reaction import Reaction from .sticker import StickerItem from .threads import Thread @@ -80,6 +81,7 @@ from .types.message import MessageApplication as MessageApplicationPayload from .types.message import MessageReference as MessageReferencePayload from .types.message import Reaction as ReactionPayload + from .types.poll import Poll as PollPayload from .types.threads import ThreadArchiveDuration from .types.user import User as UserPayload from .ui.view import View @@ -726,6 +728,10 @@ class Message(Hashable): The thread created from this message, if applicable. .. versionadded:: 2.0 + poll: Optional[:class:`Poll`] + The poll associated with this message, if applicable. + + .. versionadded:: 2.6 """ __slots__ = ( @@ -761,6 +767,7 @@ class Message(Hashable): "guild", "interaction", "thread", + "_poll", ) if TYPE_CHECKING: @@ -848,6 +855,13 @@ def __init__( except KeyError: self.interaction = None + self._poll: Poll | None + try: + self._poll = Poll.from_dict(data["poll"], self) + self._state.store_poll(self._poll, self.id) + except KeyError: + self._poll = None + self.thread: Thread | None try: self.thread = Thread( @@ -983,6 +997,10 @@ def _handle_embeds(self, value: list[EmbedPayload]) -> None: def _handle_nonce(self, value: str | int) -> None: self.nonce = value + def _handle_poll(self, value: PollPayload) -> None: + self._poll = Poll.from_dict(value, self) + self._state.store_poll(self._poll, self.id) + def _handle_author(self, author: UserPayload) -> None: self.author = self._state.store_user(author) if isinstance(self.guild, Guild): @@ -1138,6 +1156,10 @@ def jump_url(self) -> str: guild_id = getattr(self.guild, "id", "@me") return f"https://discord.com/channels/{guild_id}/{self.channel.id}/{self.id}" + @property + def poll(self) -> Poll | None: + return self._state._polls.get(self.id) + def is_system(self) -> bool: """Whether the message is a system message. @@ -1816,6 +1838,34 @@ async def reply(self, content: str | None = None, **kwargs) -> Message: return await self.channel.send(content, reference=self, **kwargs) + async def end_poll(self) -> Message: + """|coro| + + Immediately ends the poll associated with this message. Only doable by the poll's owner. + + .. versionadded:: 2.6 + + Returns + ------- + :class:`Message` + The updated message. + + Raises + ------ + Forbidden + You do not have permissions to end this poll. + HTTPException + Ending this poll failed. + """ + + data = await self._state.http.expire_poll( + self.channel.id, + self.id, + ) + message = Message(state=self._state, channel=self.channel, data=data) + + return message + def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: """Creates a :class:`~discord.MessageReference` from the current message. @@ -1942,6 +1992,10 @@ def created_at(self) -> datetime.datetime: """The partial message's creation time in UTC.""" return utils.snowflake_time(self.id) + @property + def poll(self) -> Poll | None: + return self._state._polls.get(self.id) + @utils.cached_slot_property("_cs_guild") def guild(self) -> Guild | None: """The guild that the partial message belongs to, if applicable.""" @@ -2087,3 +2141,31 @@ async def edit(self, **fields: Any) -> Message | None: view.message = msg self._state.store_view(view, self.id) return msg + + async def end_poll(self) -> Message: + """|coro| + + Immediately ends the poll associated with this message. Only doable by the poll's owner. + + .. versionadded:: 2.6 + + Returns + ------- + :class:`Message` + The updated message. + + Raises + ------ + Forbidden + You do not have permissions to end this poll. + HTTPException + Ending this poll failed. + """ + + data = await self._state.http.expire_poll( + self.channel.id, + self.id, + ) + message = self._state.create_message(channel=self.channel, data=data) + + return message diff --git a/discord/permissions.py b/discord/permissions.py index 75b71d57cd..3bc3dca1da 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -626,6 +626,14 @@ def set_voice_channel_status(self) -> int: """ return 1 << 48 + @flag_value + def send_polls(self) -> int: + """:class:`bool`: Returns ``True`` if a member can send polls. + + .. versionadded:: 2.6 + """ + return 1 << 49 + PO = TypeVar("PO", bound="PermissionOverwrite") @@ -745,6 +753,7 @@ class PermissionOverwrite: moderate_members: bool | None send_voice_messages: bool | None set_voice_channel_status: bool | None + send_polls: bool | None def __init__(self, **kwargs: bool | None): self._values: dict[str, bool | None] = {} diff --git a/discord/poll.py b/discord/poll.py new file mode 100644 index 0000000000..88127ffbbc --- /dev/null +++ b/discord/poll.py @@ -0,0 +1,522 @@ +""" +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 + +import datetime +from typing import TYPE_CHECKING, Any + +from . import utils +from .enums import PollLayoutType, try_enum +from .iterators import VoteIterator +from .partial_emoji import PartialEmoji + +__all__ = ( + "PollMedia", + "PollAnswer", + "PollAnswerCount", + "PollResults", + "Poll", +) + + +if TYPE_CHECKING: + from .abc import Snowflake + from .emoji import Emoji + from .message import Message, PartialMessage + from .types.poll import Poll as PollPayload + from .types.poll import PollAnswer as PollAnswerPayload + from .types.poll import PollAnswerCount as PollAnswerCountPayload + from .types.poll import PollMedia as PollMediaPayload + from .types.poll import PollResults as PollResultsPayload + + +class PollMedia: + """Represents a poll media object that supports both questions and answers. + + .. versionadded:: 2.6 + + Attributes + ---------- + text: :class:`str` + The question/answer text. May have up to 300 characters for questions and 55 characters for answers. + + emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]] + The answer's emoji. + """ + + def __init__(self, text: str, emoji: Emoji | PartialEmoji | str | None = None): + self.text: str = text + self.emoji: Emoji | PartialEmoji | str | None = emoji + + def to_dict(self) -> PollMediaPayload: + dict_ = { + "text": self.text, + } + if self.emoji: + if isinstance(self.emoji, str): + dict_["emoji"] = { + "name": self.emoji, + } + else: + if self.emoji.id: + dict_["emoji"] = { + "id": str(self.emoji.id), + } + else: + dict["emoji"] = {"name": self.emoji.name} + + return dict_ + + @classmethod + def from_dict( + cls, data: PollMediaPayload, message: Message | PartialMessage | None = None + ) -> PollMedia: + + _emoji: dict[str, Any] = data.get("emoji") or {} + if isinstance(_emoji, dict) and _emoji.get("name"): + emoji = PartialEmoji.from_dict(_emoji) + if emoji.id and message: + emoji = message._state.get_emoji(emoji.id) or emoji + else: + emoji = _emoji or None + return cls( + data["text"], + emoji, + ) + + def __repr__(self) -> str: + return f"" + + +class PollAnswer: + """Represents a poll answer object. + + .. versionadded:: 2.6 + + Attributes + ---------- + id: :class:`int` + The answer's ID. It currently starts at ``1`` for the first answer, then goes up sequentially. + It may not be reliable to depend on this. + media: :class:`PollMedia` + The relevant media for this answer. + """ + + def __init__(self, text: str, emoji: Emoji | PartialEmoji | str | None = None): + self.media = PollMedia(text, emoji) + self.id = None + self._poll = None + + @property + def text(self) -> str: + """The answer's text. Shortcut for :attr:`PollMedia.text`.""" + return self.media.text + + @property + def emoji(self) -> Emoji | PartialEmoji | None: + """The answer's emoji. Shortcut for :attr:`PollMedia.emoji`.""" + return self.media.emoji + + @property + def count(self) -> int | None: + """This answer's vote count, if recieved from Discord.""" + if not (self._poll and self.id): + return None + if self._poll.results is None: + return None # Unknown vote count. + _count = self._poll.results and utils.get( + self._poll.results.answer_counts, id=self.id + ) + if _count: + return _count.count + return 0 # If an answer isn't in answer_counts, it has 0 votes. + + def to_dict(self) -> PollAnswerPayload: + dict_ = { + "poll_media": self.media.to_dict(), + } + if self.id is not None: + dict_["answer_id"] = (self.id,) + return dict_ + + @classmethod + def from_dict( + cls, + data: PollAnswerPayload, + poll=None, + message: Message | PartialMessage | None = None, + ) -> PollAnswer: + media = PollMedia.from_dict(data["poll_media"], message=message) + answer = cls( + media.text, + media.emoji, + ) + answer.id = data["answer_id"] + answer._poll = poll + return answer + + def __repr__(self) -> str: + return f"" + + def voters( + self, *, limit: int | None = None, after: Snowflake | None = None + ) -> VoteIterator: + """Returns an :class:`AsyncIterator` representing the users that have voted with this answer. + Only works if this poll was recieved from Discord. + + The ``after`` parameter must represent a member + and meet the :class:`abc.Snowflake` abc. + + Parameters + ---------- + limit: Optional[:class:`int`] + The maximum number of results to return. + If not provided, returns all the users who + voted with this answer. + after: Optional[:class:`abc.Snowflake`] + For pagination, answers are sorted by member. + + Yields + ------ + Union[:class:`User`, :class:`Member`] + The member (if retrievable) or the user that has voted + with this answer. The case where it can be a :class:`Member` is + in a guild message context. Sometimes it can be a :class:`User` + if the member has left the guild. + + Raises + ------ + HTTPException + Getting the voters for the answer failed. + RuntimeError + This poll wasn't recieved from a message. + + Examples + -------- + + Usage :: + + async for user in answer.users(): + print(f'{user} voted **{answer.text}**!') + + Flattening into a list: :: + + users = await answer.users().flatten() + # users is now a list of User... + winner = random.choice(users) + await channel.send(f'{winner} has won the raffle.') + """ + + if not self._poll or not self._poll._message: + raise RuntimeError( + "Users can only be fetched from an existing message poll." + ) + + if limit is None: + limit = self.count or 100 # Ambiguous + + return VoteIterator(self._poll._message, self, limit, after) + + +class PollAnswerCount: + """Represents a poll answer count object. + + .. versionadded:: 2.6 + + Attributes + ---------- + id: :class:`int` + The answer's ID. It currently starts at ``1`` for the first answer, then goes up sequentially. + It may not be reliable to depend on this. + count: :class:`int` + The number of votes for this answer. + me: :class:`bool` + If the current user voted this answer. This is always ``False`` for bots. + """ + + def __init__(self, data: PollAnswerCountPayload): + self.id = data["id"] + self.count: int = data.get("count", 0) + self.me = data.get("me_voted") + + def to_dict(self) -> PollAnswerCountPayload: + return {"id": self.id, "count": self.count, "me_voted": self.me} + + def __repr__(self) -> str: + return f"" + + +class PollResults: + """Represents a poll results object. + + .. versionadded:: 2.6 + + Attributes + ---------- + is_finalized: :class:`bool` + Whether the poll has ended and all answer counts have been precisely tallied. + + answer_counts: List[:class:`PollAnswerCount`] + A list of counts for each answer. If an answer isn't included, it has no votes. + """ + + def __init__(self, data: PollResultsPayload): + self.is_finalized = data.get("is_finalized") + self._answer_counts = { + a["id"]: PollAnswerCount(a) for a in data.get("answer_counts", []) + } + + def to_dict(self) -> PollResultsPayload: + return { + "is_finalized": self.is_finalized, + "answer_counts": [a.to_dict() for a in self.answer_counts], + } + + def __repr__(self) -> str: + return f"" + + @property + def answer_counts(self) -> list[PollAnswerCount]: + return list(self._answer_counts.values()) + + def total_votes(self) -> int: + """ + Get the total number of votes across all answers. This may not be accurate if :attr:`is_finalized` is ``False``. + + Returns + ------- + :class:`int` + The total number of votes on this poll. + """ + return sum([a.count for a in self.answer_counts]) + + +class Poll: + """Represents a Poll. Polls are sent in regular messages, and you must have :attr:`~discord.Permissions.send_polls` to send them. + + .. versionadded:: 2.6 + + Attributes + ---------- + question: Union[:class:`PollMedia`, :class:`str`] + The poll's question media, or a ``str`` representing the question text. Question text can be up to 300 characters. + answers: Optional[List[:class:`PollAnswer`]] + A list of the poll's answers. A maximum of 10 answers can be set. + duration: :class:`int` + The number of hours until this poll expires. Users must specify this when creating a poll, but existing polls return :attr:`expiry` instead. Defaults to 24. + allow_multiselect: :class:`bool` + Whether multiple answers can be selected. Defaults to ``False``. + layout_type: :class:`PollLayoutType` + The poll's layout type. Only one exists at the moment. + results: Optional[:class:`PollResults`] + The results of this poll recieved from Discord. If ``None``, this should be considered "unknown" rather than "no" results. + """ + + def __init__( + self, + question: PollMedia | str, + *, + answers: list[PollAnswer] | None = None, + duration: int | None = 24, + allow_multiselect: bool | None = False, + layout_type: PollLayoutType | None = PollLayoutType.default, + ): + self.question = ( + question if isinstance(question, PollMedia) else PollMedia(question) + ) + self.answers: list[PollAnswer] = answers or [] + self.duration: int | None = duration + self.allow_multiselect: bool = allow_multiselect + self.layout_type: PollLayoutType = layout_type + self.results = None + self._expiry = None + self._message = None + + @utils.cached_property + def expiry(self) -> datetime.datetime | None: + """An aware datetime object that specifies the date and time in UTC when the poll will end.""" + return utils.parse_time(self._expiry) + + def to_dict(self) -> PollPayload: + dict_ = { + "question": self.question.to_dict(), + "answers": [a.to_dict() for a in self.answers], + "duration": self.duration, + "allow_multiselect": self.allow_multiselect, + "layout_type": self.layout_type.value, + } + if self.results: + dict_["results"] = [r.to_dict() for r in self.results] + if self._expiry: + dict_["expiry"] = self._expiry + return dict_ + + @classmethod + def from_dict( + cls, data: PollPayload, message: Message | PartialMessage | None = None + ) -> Poll: + if not data: + return None + poll = cls( + question=PollMedia.from_dict(data["question"], message=message), + answers=[ + PollAnswer.from_dict(a, message=message) + for a in data.get("answers", []) + ], + duration=data.get("duration"), + allow_multiselect=data.get("allow_multiselect"), + layout_type=try_enum(PollLayoutType, data.get("layout_type", 1)), + ) + if (results := data.get("results")) is not None: + poll.results = PollResults(results) + elif message and message.poll: + # results is nullable, so grab old results if necessary. + poll.results = message.poll.results + poll._expiry = data.get("expiry") + poll._message = message + for a in poll.answers: + a._poll = poll + return poll + + def __repr__(self) -> str: + return f"" + + def has_ended(self) -> bool | None: + """ + Checks if this poll has completely ended. Shortcut for :attr:`PollResults.is_finalized`, if available. + + Returns + ------- + Optional[:class:`bool`] + Returns a boolean if :attr:`results` is available, otherwise ``None``. + """ + if not self.results: + return None + return self.results.is_finalized + + def total_votes(self) -> int | None: + """ + Shortcut for :meth:`PollResults.total_votes` This may not be precise if :attr:`is_finalized` is ``False``. + + Returns + ------- + Optional[:class:`int`] + The total number of votes on this poll if :attr:`results` is available, otherwise ``None``. + """ + if not self.results: + return None + return self.results.total_votes() + + def get_answer(self, id) -> PollAnswer | None: + """ + Get a poll answer by ID. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`.PollAnswer`] + The returned answer or ``None`` if not found. + """ + return utils.get(self.answers, id=id) + + def add_answer( + self, text: str, *, emoji: Emoji | PartialEmoji | str | None = None + ) -> Poll: + """Add an answer to this poll. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + text: :class:`str` + The answer text. Maximum 55 characters. + emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]] + The answer's emoji. + + Raises + ------ + ValueError + The poll already has 10 answers or ``text`` exceeds the character length. + RuntimeError + You cannot add an answer to an existing poll. + + Examples + -------- + + Regular usage :: + + poll = Poll( + question=PollMedia("What's your favorite color?"), + + answers=[PollAnswer("Red", "❤")] + duration=24, + allow_multiselect=False + ) + poll.add_answer(text="Green", emoji="💚") + poll.add_answer(text="Blue", emoji="💙") + + Chaining style :: + + poll = Poll("What's your favorite color?").add_answer("Red", emoji="❤").add_answer("Green").add_answer("Blue") + """ + if len(self.answers) >= 10: + raise ValueError("Polls may only have up to 10 answers.") + if len(text) > 55: + raise ValueError("text length must be between 1 and 55 characters.") + if self.expiry or self._message: + raise RuntimeError("You cannot add answers to an existing poll.") + + self.answers.append(PollAnswer(text, emoji)) + return self + + async def end(self) -> Message: + """ + Immediately ends this poll, if attached to a message. Only doable by the poll's owner. + Shortcut to :meth:`Message.end_poll` + + Returns + ------- + :class:`Message` + The updated message. + + Raises + ------ + Forbidden + You do not have permissions to end this poll. + HTTPException + Ending this poll failed. + RuntimeError + This poll wasn't recieved from a message. + """ + + if not self._message: + raise RuntimeError("You can only end a poll recieved from a message.") + + return await self._message.end_poll() diff --git a/discord/raw_models.py b/discord/raw_models.py index 94226ff459..e59507dd48 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -46,6 +46,7 @@ IntegrationDeleteEvent, MemberRemoveEvent, MessageDeleteEvent, + MessagePollVoteEvent, MessageUpdateEvent, ReactionActionEvent, ReactionClearEmojiEvent, @@ -77,6 +78,7 @@ "RawThreadMembersUpdateEvent", "RawAuditLogEntryEvent", "RawVoiceChannelStatusUpdateEvent", + "RawMessagePollVoteEvent", ) @@ -780,3 +782,42 @@ def __init__(self, data: AuditLogEntryEvent) -> None: self.extra = data.get("options") self.changes = data.get("changes") self.data: AuditLogEntryEvent = data + + +class RawMessagePollVoteEvent(_RawReprMixin): + """Represents the payload for a :func:`on_message_poll_vote` event. + + .. versionadded:: 2.6 + + Attributes + ---------- + user_id: :class:`int`: + The user that added or removed their vote + message_id: :class:`int` + The message ID of the poll that received the vote. + channel_id: :class:`int` + The channel ID where the vote was updated. + guild_id: Optional[:class:`int`] + The guild ID where the vote was updated, if applicable. + answer_id: :class:`int` + The answer ID of the vote that was updated. + added: :class:`bool` + Whether this vote was added or removed. + data: :class:`dict` + The raw data sent by the `gateway ` + """ + + __slots__ = ("user_id", "message_id", "channel_id", "guild_id", "data", "added") + + def __init__(self, data: MessagePollVoteEvent, added: bool) -> None: + self.user_id: int = int(data["user_id"]) + self.channel_id: int = int(data["channel_id"]) + self.message_id: int = int(data["message_id"]) + self.answer_id: int = int(data["answer_id"]) + self.data: MessagePollVoteEvent = data + self.added: bool = added + + try: + self.guild_id: int | None = int(data["guild_id"]) + except KeyError: + self.guild_id: int | None = None diff --git a/discord/state.py b/discord/state.py index c3ae777f5f..c8d8d4dced 100644 --- a/discord/state.py +++ b/discord/state.py @@ -62,6 +62,7 @@ from .monetization import Entitlement from .object import Object from .partial_emoji import PartialEmoji +from .poll import Poll, PollAnswerCount from .raw_models import * from .role import Role from .scheduled_events import ScheduledEvent @@ -84,6 +85,7 @@ from .types.emoji import Emoji as EmojiPayload from .types.guild import Guild as GuildPayload from .types.message import Message as MessagePayload + from .types.poll import Poll as PollPayload from .types.sticker import GuildSticker as GuildStickerPayload from .types.user import User as UserPayload from .voice_client import VoiceClient @@ -274,6 +276,7 @@ def clear(self, *, views: bool = True) -> None: self._emojis: dict[int, Emoji] = {} self._stickers: dict[int, GuildSticker] = {} self._guilds: dict[int, Guild] = {} + self._polls: dict[int, Guild] = {} if views: self._view_store: ViewStore = ViewStore(self) self._modal_store: ModalStore = ModalStore(self) @@ -433,6 +436,25 @@ def get_sticker(self, sticker_id: int | None) -> GuildSticker | None: # the keys of self._stickers are ints return self._stickers.get(sticker_id) # type: ignore + @property + def polls(self) -> list[Poll]: + return list(self._polls.values()) + + def store_raw_poll(self, poll: PollPayload, raw): + channel = self.get_channel(raw.channel_id) or PartialMessageable( + state=self, id=raw.channel_id + ) + message = channel.get_partial_message(raw.message_id) + p = Poll.from_dict(poll, message) + self._polls[message.id] = p + return p + + def store_poll(self, poll: Poll, message_id: int): + self._polls[message_id] = poll + + def get_poll(self, message_id): + return self._polls.get(message_id) + @property def private_channels(self) -> list[PrivateChannel]: return list(self._private_channels.values()) @@ -730,6 +752,8 @@ def parse_message_update(self, data) -> None: older_message.author = message.author self.dispatch("message_edit", older_message, message) else: + if poll_data := data.get("poll"): + self.store_raw_poll(poll_data, raw) self.dispatch("raw_message_edit", raw) if "components" in data and self._view_store.is_message_tracked(raw.message_id): @@ -821,6 +845,56 @@ def parse_message_reaction_remove_emoji(self, data) -> None: if reaction: self.dispatch("reaction_clear_emoji", reaction) + def parse_message_poll_vote_add(self, data) -> None: + raw = RawMessagePollVoteEvent(data, True) + guild = self._get_guild(raw.guild_id) + if guild: + user = guild.get_member(raw.user_id) + else: + user = self.get_user(raw.user_id) + self.dispatch("raw_poll_vote_add", raw) + + self._get_message(raw.message_id) + poll = self.get_poll(raw.message_id) + # if message was cached, poll has already updated but votes haven't + if poll and poll.results: + answer = poll.get_answer(raw.answer_id) + counts = poll.results._answer_counts + if answer is not None: + if answer.id in counts: + counts[answer.id].count += 1 + else: + counts[answer.id] = PollAnswerCount( + {"id": answer.id, "count": 1, "me_voted": False} + ) + if poll is not None and user is not None: + answer = poll.get_answer(raw.answer_id) + if answer is not None: + self.dispatch("poll_vote_add", poll, user, answer) + + def parse_message_poll_vote_remove(self, data) -> None: + raw = RawMessagePollVoteEvent(data, False) + guild = self._get_guild(raw.guild_id) + if guild: + user = guild.get_member(raw.user_id) + else: + user = self.get_user(raw.user_id) + self.dispatch("raw_poll_vote_remove", raw) + + self._get_message(raw.message_id) + poll = self.get_poll(raw.message_id) + # if message was cached, poll has already updated but votes haven't + if poll and poll.results: + answer = poll.get_answer(raw.answer_id) + counts = poll.results._answer_counts + if answer is not None: + if answer.id in counts: + counts[answer.id].count -= 1 + if poll is not None and user is not None: + answer = poll.get_answer(raw.answer_id) + if answer is not None: + self.dispatch("poll_vote_remove", poll, user, answer) + def parse_interaction_create(self, data) -> None: interaction = Interaction(data=data, state=self) if data["type"] == 3: # interaction component diff --git a/discord/types/message.py b/discord/types/message.py index ee453f9694..e8151cb7b5 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -32,6 +32,7 @@ from .embed import Embed from .emoji import PartialEmoji from .member import Member, UserWithMember +from .poll import Poll from .snowflake import Snowflake, SnowflakeList from .sticker import StickerItem from .threads import Thread @@ -140,6 +141,7 @@ class Message(TypedDict): embeds: list[Embed] pinned: bool type: MessageType + poll: Poll AllowedMentionType = Literal["roles", "users", "everyone"] diff --git a/discord/types/poll.py b/discord/types/poll.py new file mode 100644 index 0000000000..21e54431e6 --- /dev/null +++ b/discord/types/poll.py @@ -0,0 +1,63 @@ +""" +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, TypedDict + +from .._typed_dict import NotRequired +from .emoji import Emoji + +PollLayoutType = Literal[1] + + +class PollMedia(TypedDict): + text: str + emoji: NotRequired[Emoji] + + +class PollAnswer(TypedDict): + answer_id: int + poll_media: PollMedia + + +class PollResults(TypedDict): + is_finalized: bool + answer_counts: list[PollAnswerCount] + + +class PollAnswerCount(TypedDict): + id: int + count: int + me_voted: bool + + +class Poll(TypedDict): + question: PollMedia + answers: list[PollAnswer] + duration: NotRequired[int] + expiry: NotRequired[str] + allow_multiselect: bool + layout_type: NotRequired[PollLayoutType] + results: NotRequired[PollResults] diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index de2b3fbf2b..2d0698eca3 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.py @@ -154,3 +154,11 @@ class AuditLogEntryEvent(TypedDict): changes: NotRequired[list[dict]] reason: NotRequired[str] options: NotRequired[dict] + + +class MessagePollVoteEvent(TypedDict): + user_id: Snowflake + guild_id: NotRequired[Snowflake] + channel_id: Snowflake + message_id: Snowflake + answer_id: int diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 37e8d4c894..6d8f7b35c5 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -74,6 +74,7 @@ from ..guild import Guild from ..http import Response from ..mentions import AllowedMentions + from ..poll import Poll from ..state import ConnectionState from ..types.message import Message as MessagePayload from ..types.webhook import FollowerWebhook as FollowerWebhookPayload @@ -622,6 +623,7 @@ def handle_message_parameters( embed: Embed | None = MISSING, embeds: list[Embed] = MISSING, view: View | None = MISSING, + poll: Poll | None = MISSING, applied_tags: list[Snowflake] = MISSING, allowed_mentions: AllowedMentions | None = MISSING, previous_allowed_mentions: AllowedMentions | None = None, @@ -647,6 +649,8 @@ def handle_message_parameters( if view is not MISSING: payload["components"] = view.to_components() if view is not None else [] + if poll is not MISSING: + payload["poll"] = poll.to_dict() payload["tts"] = tts if avatar_url: payload["avatar_url"] = str(avatar_url) @@ -1569,6 +1573,7 @@ async def send( embeds: list[Embed] = MISSING, allowed_mentions: AllowedMentions = MISSING, view: View = MISSING, + poll: Poll = MISSING, thread: Snowflake = MISSING, thread_name: str | None = None, applied_tags: list[Snowflake] = MISSING, @@ -1591,6 +1596,7 @@ async def send( embeds: list[Embed] = MISSING, allowed_mentions: AllowedMentions = MISSING, view: View = MISSING, + poll: Poll = MISSING, thread: Snowflake = MISSING, thread_name: str | None = None, applied_tags: list[Snowflake] = MISSING, @@ -1612,6 +1618,7 @@ async def send( embeds: list[Embed] = MISSING, allowed_mentions: AllowedMentions = MISSING, view: View = MISSING, + poll: Poll = MISSING, thread: Snowflake = MISSING, thread_name: str | None = None, applied_tags: list[Snowflake] = MISSING, @@ -1693,6 +1700,10 @@ async def send( delete_after: :class:`float` If provided, the number of seconds to wait in the background before deleting the message we just sent. + poll: :class:`Poll` + The poll to send. + + .. versionadded:: 2.6 Returns ------- @@ -1752,6 +1763,9 @@ async def send( if ephemeral is True and view.timeout is None: view.timeout = 15 * 60.0 + if poll is None: + poll = MISSING + params = handle_message_parameters( content=content, username=username, @@ -1763,6 +1777,7 @@ async def send( embeds=embeds, ephemeral=ephemeral, view=view, + poll=poll, applied_tags=applied_tags, allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst index 5df7a15fd4..f0508ac09b 100644 --- a/docs/api/data_classes.rst +++ b/docs/api/data_classes.rst @@ -93,6 +93,34 @@ Embed .. autoclass:: EmbedProvider :members: +Poll +~~~~~ + +.. attributetable:: Poll + +.. autoclass:: Poll + :members: + +.. attributetable:: PollMedia + +.. autoclass:: PollMedia + :members: + +.. attributetable:: PollAnswer + +.. autoclass:: PollAnswer + :members: + +.. attributetable:: PollAnswerCount + +.. autoclass:: PollAnswerCount + :members: + +.. attributetable:: PollResults + +.. autoclass:: PollResults + :members: + Flags diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 0500e89318..f3385c81ae 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2402,3 +2402,14 @@ of :class:`enum.Enum`. .. attribute:: user Entitlement is owned by a user. + + +.. class:: PollLayoutType + + Represents a poll's layout type. + + .. versionadded:: 2.6 + + .. attribute:: default + + Represents the default layout. diff --git a/docs/api/events.rst b/docs/api/events.rst index 25edc139b0..1b011fa1f4 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -877,6 +877,7 @@ Messages - The message's embeds were suppressed or unsuppressed. - A call message has received an update to its participants or ending time. + - A poll has ended and the results have been finalized. This requires :attr:`Intents.messages` to be enabled. @@ -909,6 +910,58 @@ Messages :param payload: The raw event payload data. :type payload: :class:`RawMessageUpdateEvent` +Polls +~~~~~~~~~ +.. function:: on_poll_vote_add(poll, user, answer) + + Called when a vote is cast on a poll. If multiple answers were selected, this fires multiple times. + if the poll was not found in the internal poll cache, then this + event will not be called. Consider using :func:`on_raw_poll_vote_add` instead. + + This requires :attr:`Intents.polls` to be enabled. + + :param poll: The current state of the poll. + :type poll: :class:`Poll` + :param user: The user who added the vote. + :type user: Union[:class:`Member`, :class:`User`] + :param answer: The answer that was voted. + :type answer: :class:`PollAnswer` + +.. function:: on_raw_poll_vote_add(payload) + + Called when a vote is cast on a poll. Unlike :func:`on_poll_vote_add`, this is + called regardless of the state of the internal poll cache. + + This requires :attr:`Intents.polls` to be enabled. + + :param payload: The raw event payload data. + :type payload: :class:`RawMessagePollVoteEvent` + +.. function:: on_poll_vote_remove(message, user, answer) + + Called when a vote is removed from a poll. If multiple answers were removed, this fires multiple times. + if the poll is not found in the internal poll cache, then this + event will not be called. Consider using :func:`on_raw_poll_vote_remove` instead. + + This requires :attr:`Intents.polls` to be enabled. + + :param poll: The current state of the poll. + :type poll: :class:`Poll` + :param user: The user who removed the vote. + :type user: Union[:class:`Member`, :class:`User`] + :param answer: The answer that was voted. + :type answer: :class:`PollAnswer` + +.. function:: on_raw_poll_vote_remove(payload) + + Called when a vote is removed from a poll. Unlike :func:`on_poll_vote_remove`, this is + called regardless of the state of the internal message cache. + + This requires :attr:`Intents.polls` to be enabled. + + :param payload: The raw event payload data. + :type payload: :class:`RawMessagePollVoteEvent` + Reactions ~~~~~~~~~ .. function:: on_reaction_add(reaction, user) From 58bd10f633655372453fb85e78a14d152db73c7a Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sun, 16 Jun 2024 23:47:42 +0200 Subject: [PATCH 084/108] fix: wrong var name (#2475) Signed-off-by: Lala Sabathil --- discord/poll.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/poll.py b/discord/poll.py index 88127ffbbc..25c964cfec 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -85,7 +85,7 @@ def to_dict(self) -> PollMediaPayload: "id": str(self.emoji.id), } else: - dict["emoji"] = {"name": self.emoji.name} + dict_["emoji"] = {"name": self.emoji.name} return dict_ From 731fd02fdfa6c3318ad2d21ea0b31592134426d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Jun 2024 22:38:38 +0000 Subject: [PATCH 085/108] ci(deps): bump ribtoks/tdg-github-action from 0.4.10.pre.beta to 0.4.11.pre.beta (#2473) ci(deps): bump ribtoks/tdg-github-action Bumps [ribtoks/tdg-github-action](https://github.com/ribtoks/tdg-github-action) from 0.4.10.pre.beta to 0.4.11.pre.beta. - [Release notes](https://github.com/ribtoks/tdg-github-action/releases) - [Commits](https://github.com/ribtoks/tdg-github-action/compare/v0.4.10-beta...v0.4.11-beta) --- updated-dependencies: - dependency-name: ribtoks/tdg-github-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/todo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml index 6f4ba8a5b6..08271e51e8 100644 --- a/.github/workflows/todo.yml +++ b/.github/workflows/todo.yml @@ -6,7 +6,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run tdg-github-action - uses: ribtoks/tdg-github-action@v0.4.10-beta + uses: ribtoks/tdg-github-action@v0.4.11-beta with: TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} From 9ab4bd6094e067e0c7956b316f483c6730ff50d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:27:45 +0000 Subject: [PATCH 086/108] chore(deps-dev): bump flake8 from 7.0.0 to 7.1.0 (#2476) Bumps [flake8](https://github.com/pycqa/flake8) from 7.0.0 to 7.1.0. - [Commits](https://github.com/pycqa/flake8/compare/7.0.0...7.1.0) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index c523385729..700320761b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,4 +8,4 @@ coverage~=7.5 pre-commit==3.5.0 codespell==2.3.0 bandit==1.7.9 -flake8==7.0.0 +flake8==7.1.0 From 284d40c70fb5143ac6a2da259373ffb080480c5f Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sun, 23 Jun 2024 22:43:12 +0100 Subject: [PATCH 087/108] fix: options initializing with null input type (#2464) * Prevent Option.__init__() completing with a null input type * Update changelog (for the checkbox) * style(pre-commit): auto fixes from pre-commit.com hooks * Update CHANGELOG.md Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks --------- Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/commands/options.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aabbc5b987..0034be2a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2458](https://github.com/Pycord-Development/pycord/pull/2458)) - Fixed many inaccurate type hints throughout the library. ([#2457](https://github.com/Pycord-Development/pycord/pull/2457)) +- Fixed `AttributeError` due to `discord.Option` being initialised with `input_type` set + to `None`. ([#2464](https://github.com/Pycord-Development/pycord/pull/2464)) ### Changed diff --git a/discord/commands/options.py b/discord/commands/options.py index 38cac6b539..2ca8c5a39a 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -330,6 +330,9 @@ def __init__( "description_localizations", MISSING ) + if input_type is None: + raise TypeError("input_type cannot be NoneType.") + def to_dict(self) -> dict: as_dict = { "name": self.name, From 59b919f0951ff6c838556b53ae8bc60e7a4b081f Mon Sep 17 00:00:00 2001 From: plun1331 Date: Tue, 25 Jun 2024 16:24:14 -0700 Subject: [PATCH 088/108] feat: user-installable apps (#2409) * add enums, do command stuff, add context to interaction * style(pre-commit): auto fixes from pre-commit.com hooks * add authorizing_integration_owners * style(pre-commit): auto fixes from pre-commit.com hooks * add application_metadata * style(pre-commit): auto fixes from pre-commit.com hooks * don't trust copilot * style(pre-commit): auto fixes from pre-commit.com hooks * update __all__ * circular import * style(pre-commit): auto fixes from pre-commit.com hooks * fix numbers * h * style(pre-commit): auto fixes from pre-commit.com hooks * update guild_only deco to use contexts * type * style(pre-commit): auto fixes from pre-commit.com hooks * deprecation warnings * style(pre-commit): auto fixes from pre-commit.com hooks * docs * example * style(pre-commit): auto fixes from pre-commit.com hooks * edit docs * update changelog * style(pre-commit): auto fixes from pre-commit.com hooks * update changelog * add default contexts and integration_types values * style(pre-commit): auto fixes from pre-commit.com hooks * add default contexts and integration_types values * h * style(pre-commit): auto fixes from pre-commit.com hooks * subcmds * subcmds * update check for desynced cmds * h * h * Apply suggestions from code review Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: plun1331 * Update CHANGELOG.md Seperate fix was made in #2411 Signed-off-by: plun1331 * allow Message.interaction to be settable ref. https://github.com/mahtoid/DiscordChatExporterPy/issues/106 Signed-off-by: plun1331 * Update core.py Signed-off-by: plun1331 * Update interactions.py Signed-off-by: plun1331 * style(pre-commit): auto fixes from pre-commit.com hooks * Update core.py Signed-off-by: plun1331 * style(pre-commit): auto fixes from pre-commit.com hooks * Update core.py Signed-off-by: plun1331 * Apply suggestions from code review Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: plun1331 * Update permissions.py Signed-off-by: plun1331 * Update permissions.py Signed-off-by: plun1331 * style(pre-commit): auto fixes from pre-commit.com hooks * Update interactions.py Signed-off-by: plun1331 * Apply suggestions from code review Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * Update discord/interactions.py Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks --------- Signed-off-by: plun1331 Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Co-authored-by: Lala Sabathil Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 7 ++ discord/bot.py | 52 +++++++- discord/commands/core.py | 173 +++++++++++++++++++++----- discord/commands/permissions.py | 11 +- discord/enums.py | 17 +++ discord/interactions.py | 179 ++++++++++++++++++++++++++- discord/message.py | 48 ++++++- discord/permissions.py | 12 ++ discord/types/interactions.py | 21 +++- discord/types/message.py | 3 +- docs/api/enums.rst | 34 +++++ docs/api/models.rst | 10 ++ examples/app_commands/slash_users.py | 33 +++++ 13 files changed, 556 insertions(+), 44 deletions(-) create mode 100644 examples/app_commands/slash_users.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0034be2a32..32b1ae5f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2408](https://github.com/Pycord-Development/pycord/pull/2408)) - Added `stacklevel` param to `utils.warn_deprecated` and `utils.deprecated`. ([#2450](https://github.com/Pycord-Development/pycord/pull/2450)) +- Added support for user-installable applications. + ([#2409](https://github.com/Pycord-Development/pycord/pull/2409) ### Fixed @@ -71,6 +73,11 @@ These changes are available on the `master` branch, but have not yet been releas ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) - `Guild.query_members` now accepts `limit=None` to retrieve all members. ([#2419](https://github.com/Pycord-Development/pycord/pull/2419)) +- `ApplicationCommand.guild_only` is now deprecated in favor of + `ApplicationCommand.contexts`. + ([#2409](https://github.com/Pycord-Development/pycord/pull/2409)) +- `Message.interaction` is now deprecated in favor of `Message.interaction_metadata`. + ([#2409](https://github.com/Pycord-Development/pycord/pull/2409) ### Removed diff --git a/discord/bot.py b/discord/bot.py index 7d561b52cc..19a1ca9bdc 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -48,7 +48,7 @@ UserCommand, command, ) -from .enums import InteractionType +from .enums import IntegrationType, InteractionContextType, InteractionType from .errors import CheckFailure, DiscordException from .interactions import Interaction from .shard import AutoShardedClient @@ -125,6 +125,13 @@ def add_application_command(self, command: ApplicationCommand) -> None: if self._bot.debug_guilds and command.guild_ids is None: command.guild_ids = self._bot.debug_guilds + if self._bot.default_command_contexts and command.contexts is None: + command.contexts = self._bot.default_command_contexts + if ( + self._bot.default_command_integration_types + and command.integration_types is None + ): + command.integration_types = self._bot.default_command_integration_types for cmd in self.pending_application_commands: if cmd == command: @@ -271,7 +278,6 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: else: as_dict = cmd.to_dict() to_check = { - "dm_permission": None, "nsfw": None, "default_member_permissions": None, "name": None, @@ -287,6 +293,8 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: "name_localizations", "description_localizations", ], + "contexts": None, + "integration_types": None, } for check, value in to_check.items(): if type(to_check[check]) == list: @@ -1157,6 +1165,21 @@ def __init__(self, description=None, *args, **options): self.auto_sync_commands = options.get("auto_sync_commands", True) self.debug_guilds = options.pop("debug_guilds", None) + self.default_command_contexts = options.pop( + "default_command_contexts", + { + InteractionContextType.guild, + InteractionContextType.bot_dm, + InteractionContextType.private_channel, + }, + ) + + self.default_command_integration_types = options.pop( + "default_command_integration_types", + { + IntegrationType.guild_install, + }, + ) if self.owner_id and self.owner_ids: raise TypeError("Both owner_id and owner_ids are set.") @@ -1167,6 +1190,20 @@ def __init__(self, description=None, *args, **options): raise TypeError( f"owner_ids must be a collection not {self.owner_ids.__class__!r}" ) + if not isinstance(self.default_command_contexts, collections.abc.Collection): + raise TypeError( + f"default_command_contexts must be a collection not {self.default_command_contexts.__class__!r}" + ) + if not isinstance( + self.default_command_integration_types, collections.abc.Collection + ): + raise TypeError( + f"default_command_integration_types must be a collection not {self.default_command_integration_types.__class__!r}" + ) + self.default_command_contexts = set(self.default_command_contexts) + self.default_command_integration_types = set( + self.default_command_integration_types + ) self._checks = [] self._check_once = [] @@ -1447,6 +1484,17 @@ class Bot(BotBase, Client): :attr:`.process_application_commands` if the command is not found. Defaults to ``True``. .. versionadded:: 2.0 + default_command_contexts: Collection[:class:`InteractionContextType`] + The default context types that the bot will use for commands. + Defaults to a set containing :attr:`InteractionContextType.guild`, :attr:`InteractionContextType.bot_dm`, and + :attr:`InteractionContextType.private_channel`. + + .. versionadded:: 2.6 + default_command_integration_types: Collection[:class:`IntegrationType`]] + The default integration types that the bot will use for commands. + Defaults to a set containing :attr:`IntegrationType.guild_install`. + + .. versionadded:: 2.6 """ @property diff --git a/discord/commands/core.py b/discord/commands/core.py index 26469472e8..d89ea0a5b2 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -45,14 +45,21 @@ Union, ) -from ..channel import _threaded_guild_channel_factory +from ..channel import PartialMessageable, _threaded_guild_channel_factory from ..enums import Enum as DiscordEnum -from ..enums import MessageType, SlashCommandOptionType, try_enum +from ..enums import ( + IntegrationType, + InteractionContextType, + MessageType, + SlashCommandOptionType, + try_enum, +) from ..errors import ( ApplicationCommandError, ApplicationCommandInvokeError, CheckFailure, ClientException, + InvalidArgument, ValidationError, ) from ..member import Member @@ -61,7 +68,7 @@ from ..role import Role from ..threads import Thread from ..user import User -from ..utils import MISSING, async_all, find, maybe_coroutine, utcnow +from ..utils import MISSING, async_all, find, maybe_coroutine, utcnow, warn_deprecated from .context import ApplicationContext, AutocompleteContext from .options import Option, OptionChoice @@ -226,11 +233,36 @@ def __init__(self, func: Callable, **kwargs) -> None: "__default_member_permissions__", kwargs.get("default_member_permissions", 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)) + integration_types = getattr( + func, "__integration_types__", kwargs.get("integration_types", None) + ) + contexts = getattr(func, "__contexts__", kwargs.get("contexts", None)) + guild_only = getattr(func, "__guild_only__", kwargs.get("guild_only", MISSING)) + if guild_only is not MISSING: + warn_deprecated( + "guild_only", + "contexts", + "2.6", + reference="https://discord.com/developers/docs/change-log#userinstallable-apps-preview", + ) + if contexts and guild_only: + raise InvalidArgument( + "cannot pass both 'contexts' and 'guild_only' to ApplicationCommand" + ) + if self.guild_ids and ( + (contexts is not None) or guild_only or integration_types + ): + raise InvalidArgument( + "the 'contexts' and 'integration_types' parameters are not available for guild commands" + ) + + if guild_only: + contexts = {InteractionContextType.guild} + self.contexts: set[InteractionContextType] | None = contexts + self.integration_types: set[IntegrationType] | None = integration_types + def __repr__(self) -> str: return f"" @@ -274,6 +306,33 @@ def callback( unwrap = unwrap_function(function) self.module = unwrap.__module__ + @property + def guild_only(self) -> bool: + warn_deprecated( + "guild_only", + "contexts", + "2.6", + reference="https://discord.com/developers/docs/change-log#userinstallable-apps-preview", + ) + return InteractionContextType.guild in self.contexts and len(self.contexts) == 1 + + @guild_only.setter + def guild_only(self, value: bool) -> None: + warn_deprecated( + "guild_only", + "contexts", + "2.6", + reference="https://discord.com/developers/docs/change-log#userinstallable-apps-preview", + ) + if value: + self.contexts = {InteractionContextType.guild} + else: + self.contexts = { + InteractionContextType.guild, + InteractionContextType.bot_dm, + InteractionContextType.private_channel, + } + def _prepare_cooldowns(self, ctx: ApplicationContext): if self._buckets.valid: current = datetime.datetime.now().timestamp() @@ -631,6 +690,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. + + .. deprecated:: 2.6 + Use the :attr:`contexts` parameter instead. 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. @@ -654,6 +716,12 @@ class SlashCommand(ApplicationCommand): description_localizations: Dict[:class:`str`, :class:`str`] The description localizations for this command. The values of this should be ``"locale": "description"``. See `here `_ for a list of valid locales. + integration_types: Set[:class:`IntegrationType`] + The type of installation this command should be available to. For instance, if set to + :attr:`IntegrationType.user_install`, the command will only be available to users with + the application installed on their account. Unapplicable for guild commands. + contexts: Set[:class:`InteractionContextType`] + The location where this command can be used. Cannot be set if this is a guild command. """ type = 1 @@ -881,9 +949,6 @@ def to_dict(self) -> dict: if self.is_subcommand: as_dict["type"] = SlashCommandOptionType.sub_command.value - 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 @@ -892,6 +957,10 @@ def to_dict(self) -> dict: self.default_member_permissions.value ) + if not self.guild_ids and not self.is_subcommand: + as_dict["integration_types"] = [it.value for it in self.integration_types] + as_dict["contexts"] = [ctx.value for ctx in self.contexts] + return as_dict async def _invoke(self, ctx: ApplicationContext) -> None: @@ -1100,6 +1169,9 @@ class SlashCommandGroup(ApplicationCommand): isn't one. guild_only: :class:`bool` Whether the command should only be usable inside a guild. + + .. deprecated:: 2.6 + Use the :attr:`contexts` parameter instead. 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. @@ -1118,6 +1190,12 @@ class SlashCommandGroup(ApplicationCommand): description_localizations: Dict[:class:`str`, :class:`str`] The description localizations for this command. The values of this should be ``"locale": "description"``. See `here `_ for a list of valid locales. + integration_types: Set[:class:`IntegrationType`] + The type of installation this command should be available to. For instance, if set to + :attr:`IntegrationType.user_install`, the command will only be available to users with + the application installed on their account. Unapplicable for guild commands. + contexts: Set[:class:`InteractionContextType`] + The location where this command can be used. Unapplicable for guild commands. """ __initial_commands__: list[SlashCommand | SlashCommandGroup] @@ -1177,9 +1255,30 @@ def __init__( self.default_member_permissions: Permissions | None = kwargs.get( "default_member_permissions", None ) - self.guild_only: bool | None = kwargs.get("guild_only", None) self.nsfw: bool | None = kwargs.get("nsfw", None) + integration_types = kwargs.get("integration_types", None) + contexts = kwargs.get("contexts", None) + guild_only = kwargs.get("guild_only", MISSING) + if guild_only is not MISSING: + warn_deprecated("guild_only", "contexts", "2.6") + if contexts and guild_only: + raise InvalidArgument( + "cannot pass both 'contexts' and 'guild_only' to ApplicationCommand" + ) + if self.guild_ids and ( + (contexts is not None) or guild_only or integration_types + ): + raise InvalidArgument( + "the 'contexts' and 'integration_types' parameters are not available for guild commands" + ) + + # These are set to None and their defaults are then set when added to the bot + self.contexts: set[InteractionContextType] | None = contexts + if guild_only: + self.guild_only: bool | None = guild_only + self.integration_types: set[IntegrationType] | None = integration_types + self.name_localizations: dict[str, str] = kwargs.get( "name_localizations", MISSING ) @@ -1218,6 +1317,23 @@ def __init__( def module(self) -> str | None: return self.__module__ + @property + def guild_only(self) -> bool: + warn_deprecated("guild_only", "contexts", "2.6") + return InteractionContextType.guild in self.contexts and len(self.contexts) == 1 + + @guild_only.setter + def guild_only(self, value: bool) -> None: + warn_deprecated("guild_only", "contexts", "2.6") + if value: + self.contexts = {InteractionContextType.guild} + else: + self.contexts = { + InteractionContextType.guild, + InteractionContextType.bot_dm, + InteractionContextType.private_channel, + } + def to_dict(self) -> dict: as_dict = { "name": self.name, @@ -1232,9 +1348,6 @@ def to_dict(self) -> dict: if self.parent is not None: as_dict["type"] = self.input_type.value - 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 @@ -1243,6 +1356,10 @@ def to_dict(self) -> dict: self.default_member_permissions.value ) + if not self.guild_ids and self.parent is None: + as_dict["integration_types"] = [it.value for it in self.integration_types] + as_dict["contexts"] = [ctx.value for ctx in self.contexts] + return as_dict def add_command(self, command: SlashCommand | SlashCommandGroup) -> None: @@ -1476,6 +1593,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. + + .. deprecated:: 2.6 + Use the ``contexts`` parameter instead. 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. @@ -1496,6 +1616,10 @@ class ContextMenuCommand(ApplicationCommand): name_localizations: Dict[:class:`str`, :class:`str`] The name localizations for this command. The values of this should be ``"locale": "name"``. See `here `_ for a list of valid locales. + integration_types: Set[:class:`IntegrationType`] + The installation contexts where this command is available. Unapplicable for guild commands. + contexts: Set[:class:`InteractionContextType`] + The interaction contexts where this command is available. Unapplicable for guild commands. """ def __new__(cls, *args, **kwargs) -> ContextMenuCommand: @@ -1575,8 +1699,9 @@ def to_dict(self) -> dict[str, str | int]: "type": self.type, } - if self.guild_only is not None: - as_dict["dm_permission"] = not self.guild_only + if not self.guild_ids: + as_dict["integration_types"] = [it.value for it in self.integration_types] + as_dict["contexts"] = [ctx.value for ctx in self.contexts] if self.nsfw is not None: as_dict["nsfw"] = self.nsfw @@ -1729,20 +1854,12 @@ async def _invoke(self, ctx: ApplicationContext): for i, v in _data.items(): v["id"] = int(i) message = v - channel = ctx.interaction._state.get_channel(int(message["channel_id"])) - if channel is None: - author_id = int(message["author"]["id"]) - self_or_system_message: bool = ctx.bot.user.id == author_id or try_enum( - MessageType, message["type"] - ) not in ( - MessageType.default, - MessageType.reply, - MessageType.application_command, - MessageType.thread_starter_message, + channel = ctx.interaction.channel + if channel.id != int(message["channel_id"]): + # we got weird stuff going on, make up a channel + channel = PartialMessageable( + state=ctx.interaction._state, id=int(message["channel_id"]) ) - user_id = ctx.author.id if self_or_system_message else author_id - data = await ctx.interaction._state.http.start_private_message(user_id) - channel = ctx.interaction._state.add_dm_channel(data) target = Message(state=ctx.interaction._state, channel=channel, data=message) diff --git a/discord/commands/permissions.py b/discord/commands/permissions.py index df951ae001..daf633b05a 100644 --- a/discord/commands/permissions.py +++ b/discord/commands/permissions.py @@ -25,6 +25,7 @@ from typing import Callable +from ..enums import InteractionContextType from ..permissions import Permissions from .core import ApplicationCommand @@ -32,7 +33,7 @@ def default_permissions(**perms: bool) -> Callable: - """A decorator that limits the usage of a slash command to members with certain + """A decorator that limits the usage of an application command to members with certain permissions. The permissions passed in must be exactly like the properties shown under @@ -80,7 +81,7 @@ def inner(command: Callable): def guild_only() -> Callable: - """A decorator that limits the usage of a slash command to guild contexts. + """A decorator that limits the usage of an application command to guild contexts. The command won't be able to be used in private message channels. Example @@ -98,9 +99,9 @@ async def test(ctx): def inner(command: Callable): if isinstance(command, ApplicationCommand): - command.guild_only = True + command.contexts = {InteractionContextType.guild} else: - command.__guild_only__ = True + command.__contexts__ = {InteractionContextType.guild} return command @@ -108,7 +109,7 @@ def inner(command: Callable): def is_nsfw() -> Callable: - """A decorator that limits the usage of a slash command to 18+ channels and users. + """A decorator that limits the usage of an application 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. diff --git a/discord/enums.py b/discord/enums.py index be7510fffc..1446e1af7d 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -74,6 +74,8 @@ "SKUType", "EntitlementType", "EntitlementOwnerType", + "IntegrationType", + "InteractionContextType", ) @@ -1020,6 +1022,21 @@ class EntitlementOwnerType(Enum): user = 2 +class IntegrationType(Enum): + """The application's integration type""" + + guild_install = 0 + user_install = 1 + + +class InteractionContextType(Enum): + """The interaction's context type""" + + guild = 0 + bot_dm = 1 + private_channel = 2 + + class PollLayoutType(Enum): """The poll's layout type.""" diff --git a/discord/interactions.py b/discord/interactions.py index b2663e0b1d..5725796d06 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -30,7 +30,12 @@ from . import utils from .channel import ChannelType, PartialMessageable, _threaded_channel_factory -from .enums import InteractionResponseType, InteractionType, try_enum +from .enums import ( + InteractionContextType, + InteractionResponseType, + InteractionType, + try_enum, +) from .errors import ClientException, InteractionResponded, InvalidArgument from .file import File from .flags import MessageFlags @@ -53,6 +58,8 @@ "InteractionMessage", "InteractionResponse", "MessageInteraction", + "InteractionMetadata", + "AuthorizingIntegrationOwners", ) if TYPE_CHECKING: @@ -76,6 +83,7 @@ from .threads import Thread from .types.interactions import Interaction as InteractionPayload from .types.interactions import InteractionData + from .types.interactions import InteractionMetadata as InteractionMetadataPayload from .types.interactions import MessageInteraction as MessageInteractionPayload from .ui.modal import Modal from .ui.view import View @@ -99,7 +107,7 @@ class Interaction: """Represents a Discord interaction. An interaction happens when a user does an action that needs to - be notified. Current examples are slash commands and components. + be notified. Current examples are application commands, components, and modals. .. versionadded:: 2.0 @@ -132,6 +140,14 @@ class Interaction: The guilds preferred locale, if invoked in a guild. custom_id: Optional[:class:`str`] The custom ID for the interaction. + authorizing_integration_owners: :class:`AuthorizingIntegrationOwners` + Contains the entities (users or guilds) that authorized this interaction. + + .. versionadded:: 2.6 + context: Optional[:class:`InteractionContextType`] + The context in which this command was executed. + + .. versionadded:: 2.6 """ __slots__: tuple[str, ...] = ( @@ -150,6 +166,8 @@ class Interaction: "version", "custom_id", "entitlements", + "context", + "authorizing_integration_owners", "_channel_data", "_message_data", "_guild_data", @@ -189,6 +207,18 @@ def _from_data(self, data: InteractionPayload): self.entitlements: list[Entitlement] = [ Entitlement(data=e, state=self._state) for e in data.get("entitlements", []) ] + self.authorizing_integration_owners: AuthorizingIntegrationOwners = ( + AuthorizingIntegrationOwners( + data=data["authorizing_integration_owners"], state=self._state + ) + if "authorizing_integration_owners" in data + else AuthorizingIntegrationOwners(data={}, state=self._state) + ) + self.context: InteractionContextType | None = ( + try_enum(InteractionContextType, data["context"]) + if "context" in data + else None + ) self.message: Message | None = None self.channel = None @@ -1402,6 +1432,10 @@ class MessageInteraction: .. versionadded:: 2.0 + .. deprecated:: 2.6 + + See :class:`InteractionMetadata`. + .. note:: Responses to message components do not include this property. @@ -1428,3 +1462,144 @@ def __init__(self, *, data: MessageInteractionPayload, state: ConnectionState): self.type: InteractionType = data["type"] self.name: str = data["name"] self.user: User = self._state.store_user(data["user"]) + + +class InteractionMetadata: + """Represents metadata about an interaction. + + This is sent on the message object when the message is related to an interaction + + .. versionadded:: 2.6 + + Attributes + ---------- + id: :class:`int` + The interaction's ID. + type: :class:`InteractionType` + The interaction type. + user: :class:`User` + The user that sent the interaction. + authorizing_integration_owners: :class:`AuthorizingIntegrationOwners` + The authorizing user or server for the installation(s) relevant to the interaction. + original_response_message_id: Optional[:class:`int`] + The ID of the original response message. Only present on interaction follow-up messages. + interacted_message_id: Optional[:class:`int`] + The ID of the message that triggered the interaction. Only present on interactions of type + :attr:`InteractionType.component`. + triggering_interaction_metadata: Optional[:class:`InteractionMetadata`] + The metadata of the interaction that opened the model. Only present on interactions of type + :attr:`InteractionType.modal_submit`. + """ + + __slots__: tuple[str, ...] = ( + "id", + "type", + "user", + "authorizing_integration_owners", + "original_response_message_id", + "interacted_message_id", + "triggering_interaction_metadata", + "_state", + "_cs_original_response_message", + "_cs_interacted_message", + ) + + def __init__(self, *, data: InteractionMetadataPayload, state: ConnectionState): + self._state = state + self.id: int = int(data["id"]) + self.type: InteractionType = try_enum(InteractionType, data["type"]) + self.user: User = User(state=state, data=data["user"]) + self.authorizing_integration_owners: AuthorizingIntegrationOwners = ( + AuthorizingIntegrationOwners(data["authorizing_integration_owners"], state) + ) + self.original_response_message_id: int | None = utils._get_as_snowflake( + data, "original_response_message_id" + ) + self.interacted_message_id: int | None = utils._get_as_snowflake( + data, "interacted_message_id" + ) + self.triggering_interaction_metadata: InteractionMetadata | None = None + if tim := data.get("triggering_interaction_metadata"): + self.triggering_interaction_metadata = InteractionMetadata( + data=tim, state=state + ) + + def __repr__(self): + return ( + f"" + ) + + @utils.cached_slot_property("_cs_original_response_message") + def original_response_message(self) -> Message | None: + """Optional[:class:`Message`]: The original response message. + Returns ``None`` if the message is not in cache, or if :attr:`original_response_message_id` is ``None``. + """ + if not self.original_response_message_id: + return None + return self._state._get_message(self.original_response_message_id) + + @utils.cached_slot_property("_cs_interacted_message") + def interacted_message(self) -> Message | None: + """Optional[:class:`Message`]: The message that triggered the interaction. + Returns ``None`` if the message is not in cache, or if :attr:`interacted_message_id` is ``None``. + """ + if not self.interacted_message_id: + return None + return self._state._get_message(self.interacted_message_id) + + +class AuthorizingIntegrationOwners: + """Contains details on the authorizing user or server for the installation(s) relevant to the interaction. + + .. versionadded:: 2.6 + + Attributes + ---------- + user_id: :class:`int` | None + The ID of the user that authorized the integration. + guild_id: :class:`int` | None + The ID of the guild that authorized the integration. + This will be ``0`` if the integration was triggered + from the user in the bot's DMs. + """ + + __slots__ = ("user_id", "guild_id", "_state", "_cs_user", "_cs_guild") + + def __init__(self, data: dict[str, Any], state: ConnectionState): + self._state = state + # keys are Application Integration Types as strings + self.user_id = int(uid) if (uid := data.get("1")) is not None else None + self.guild_id = ( + int(guild_id) if (guild_id := data.get("0", None)) is not None else None + ) + + def __repr__(self): + return f"" + + def __eq__(self, other): + return ( + isinstance(other, AuthorizingIntegrationOwners) + and self.user_id == other.user_id + and self.guild_id == other.guild_id + ) + + def __ne__(self, other): + return not self.__eq__(other) + + @utils.cached_slot_property("_cs_user") + def user(self) -> User | None: + """Optional[:class:`User`]: The user that authorized the integration. + Returns ``None`` if the user is not in cache, or if :attr:`user_id` is ``None``. + """ + if not self.user_id: + return None + return self._state.get_user(self.user_id) + + @utils.cached_slot_property("_cs_guild") + def guild(self) -> Guild | None: + """Optional[:class:`Guild`]: The guild that authorized the integration. + Returns ``None`` if the guild is not in cache, or if :attr:`guild_id` is ``0`` or ``None``. + """ + if not self.guild_id: + return None + return self._state._get_guild(self.guild_id) diff --git a/discord/message.py b/discord/message.py index b97d8cd86c..109bef02c8 100644 --- a/discord/message.py +++ b/discord/message.py @@ -68,6 +68,7 @@ ) from .channel import TextChannel from .components import Component + from .interactions import MessageInteraction from .mentions import AllowedMentions from .role import Role from .state import ConnectionState @@ -724,6 +725,14 @@ class Message(Hashable): The guild that the message belongs to, if applicable. interaction: Optional[:class:`MessageInteraction`] The interaction associated with the message, if applicable. + + .. deprecated:: 2.6 + + Use :attr:`interaction_metadata` instead. + interaction_metadata: Optional[:class:`InteractionMetadata`] + The interaction metadata associated with the message, if applicable. + + .. versionadded:: 2.6 thread: Optional[:class:`Thread`] The thread created from this message, if applicable. @@ -765,7 +774,8 @@ class Message(Hashable): "stickers", "components", "guild", - "interaction", + "_interaction", + "interaction_metadata", "thread", "_poll", ) @@ -847,13 +857,21 @@ def __init__( # the channel will be the correct type here ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore - from .interactions import MessageInteraction + from .interactions import InteractionMetadata, MessageInteraction - self.interaction: MessageInteraction | None + self._interaction: MessageInteraction | None + try: + self._interaction = MessageInteraction( + data=data["interaction"], state=state + ) + except KeyError: + self._interaction = None try: - self.interaction = MessageInteraction(data=data["interaction"], state=state) + self.interaction_metadata = InteractionMetadata( + data=data["interaction_metadata"], state=state + ) except KeyError: - self.interaction = None + self.interaction_metadata = None self._poll: Poll | None try: @@ -1057,6 +1075,26 @@ def _rebind_cached_references( self.guild = new_guild self.channel = new_channel + @property + def interaction(self) -> MessageInteraction | None: + utils.warn_deprecated( + "interaction", + "interaction_metadata", + "2.6", + reference="https://discord.com/developers/docs/change-log#userinstallable-apps-preview", + ) + return self._interaction + + @interaction.setter + def interaction(self, value: MessageInteraction | None) -> None: + utils.warn_deprecated( + "interaction", + "interaction_metadata", + "2.6", + reference="https://discord.com/developers/docs/change-log#userinstallable-apps-preview", + ) + self._interaction = value + @utils.cached_slot_property("_cs_raw_mentions") def raw_mentions(self) -> list[int]: """A property that returns an array of user IDs matched with diff --git a/discord/permissions.py b/discord/permissions.py index 3bc3dca1da..497ef597e1 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -634,6 +634,17 @@ def send_polls(self) -> int: """ return 1 << 49 + @flag_value + def use_external_apps(self) -> int: + """:class:`bool`: Returns ``True`` if a member's user-installed apps can show public responses. + Users will still be able to use user-installed apps, but responses will be ephemeral. + + This only applies to apps that are also not installed to the guild. + + .. versionadded:: 2.6 + """ + return 1 << 50 + PO = TypeVar("PO", bound="PermissionOverwrite") @@ -754,6 +765,7 @@ class PermissionOverwrite: send_voice_messages: bool | None set_voice_channel_status: bool | None send_polls: bool | None + use_external_apps: bool | None def __init__(self, **kwargs: bool | None): self._values: dict[str, bool | None] = {} diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 8ad0295b40..2c7fd520ab 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, Union +from typing import TYPE_CHECKING, Dict, Literal, Union from ..permissions import Permissions from .channel import ChannelType @@ -221,6 +221,18 @@ class Interaction(TypedDict): token: str version: int entitlements: list[Entitlement] + authorizing_integration_owners: AuthorizingIntegrationOwners + context: InteractionContextType + + +class InteractionMetadata(TypedDict): + id: Snowflake + type: InteractionType + user_id: Snowflake + authorizing_integration_owners: AuthorizingIntegrationOwners + original_response_message_id: NotRequired[Snowflake] + interacted_message_id: NotRequired[Snowflake] + triggering_interaction_metadata: NotRequired[InteractionMetadata] class InteractionApplicationCommandCallbackData(TypedDict, total=False): @@ -253,3 +265,10 @@ class EditApplicationCommand(TypedDict): type: NotRequired[ApplicationCommandType] name: str default_permission: bool + + +InteractionContextType = Literal[0, 1, 2] +ApplicationIntegrationType = Literal[0, 1] +_StringApplicationIntegrationType = Literal["0", "1"] + +AuthorizingIntegrationOwners = Dict[_StringApplicationIntegrationType, Snowflake] diff --git a/discord/types/message.py b/discord/types/message.py index e8151cb7b5..8988891efa 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -39,7 +39,7 @@ from .user import User if TYPE_CHECKING: - from .interactions import MessageInteraction + from .interactions import InteractionMetadata, MessageInteraction from .._typed_dict import NotRequired, TypedDict @@ -125,6 +125,7 @@ class Message(TypedDict): sticker_items: NotRequired[list[StickerItem]] referenced_message: NotRequired[Message | None] interaction: NotRequired[MessageInteraction] + interaction_metadata: NotRequired[InteractionMetadata] components: NotRequired[list[Component]] thread: NotRequired[Thread | None] id: Snowflake diff --git a/docs/api/enums.rst b/docs/api/enums.rst index f3385c81ae..b54f962e47 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2413,3 +2413,37 @@ of :class:`enum.Enum`. .. attribute:: default Represents the default layout. + + +.. class:: IntegrationType + + The integration type for an application. + + .. versionadded:: 2.6 + + .. attribute:: guild_install + + The integration is added to a guild. + + .. attribute:: user_install + + The integration is added to a user account. + + +.. class:: InteractionContextType + + The context where an interaction occurs. + + .. versionadded:: 2.6 + + .. attribute:: guild + + The interaction is in a guild. + + .. attribute:: bot_dm + + The interaction is in the bot's own DM channel with the user. + + .. attribute:: private_channel + + The interaction is in a private DM or group DM channel. diff --git a/docs/api/models.rst b/docs/api/models.rst index 5fec2f0dd0..bea6b3c0de 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -353,6 +353,16 @@ Interactions .. autoclass:: MessageInteraction() :members: +.. attributetable:: InteractionMetadata + +.. autoclass:: InteractionMetadata() + :members: + +.. attributetable:: AuthorizingIntegrationOwners + +.. autoclass:: AuthorizingIntegrationOwners() + :members: + .. attributetable:: Component .. autoclass:: Component() diff --git a/examples/app_commands/slash_users.py b/examples/app_commands/slash_users.py new file mode 100644 index 0000000000..73c32cfbad --- /dev/null +++ b/examples/app_commands/slash_users.py @@ -0,0 +1,33 @@ +import discord + +# debug_guilds must not be set if we want to set contexts and integration_types on commands +bot = discord.Bot() + + +@bot.slash_command( + # Can only be used in private messages + contexts={discord.InteractionContextType.private_channel}, + # Can only be used if the bot is installed to your user account, + # if left blank it can only be used when added to guilds + integration_types={discord.IntegrationType.user_install}, +) +async def greet(ctx: discord.ApplicationContext, user: discord.User): + await ctx.respond(f"Hello, {user}!") + + +@bot.slash_command( + # This command can be used by guild members, but also by users anywhere if they install it + integration_types={ + discord.IntegrationType.guild_install, + discord.IntegrationType.user_install, + }, +) +async def say_hello(ctx: discord.ApplicationContext): + await ctx.respond("Hello!") + + +# If a bot is not installed to a guild and the channel has the `USE_EXTERNAL_APPS` +# permission disabled, the response will always be ephemeral. + + +bot.run("TOKEN") From a4b44cadf8ea927295911f491ba84a5522120680 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 03:46:53 +0300 Subject: [PATCH 089/108] chore(deps-dev): update mypy requirement from ~=1.10.0 to ~=1.10.1 (#2481) --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 700320761b..43a30c6eb5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -3,7 +3,7 @@ pylint~=3.2.3 pytest~=8.2.2 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 -mypy~=1.10.0 +mypy~=1.10.1 coverage~=7.5 pre-commit==3.5.0 codespell==2.3.0 From 7ae39287d82c3442559f00ebe0383b000681a62b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jun 2024 01:12:06 +0000 Subject: [PATCH 090/108] chore(deps-dev): update pylint requirement from ~=3.2.3 to ~=3.2.4 (#2482) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.3...v3.2.4) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 43a30c6eb5..ee9e9cd99c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r _.txt -pylint~=3.2.3 +pylint~=3.2.4 pytest~=8.2.2 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 From e65c7176d5cc6ca70b425b910a859446a5647ac3 Mon Sep 17 00:00:00 2001 From: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Date: Thu, 27 Jun 2024 21:39:16 -0700 Subject: [PATCH 091/108] docs: limits on integer and number option types (#2462) Signed-off-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: Lala Sabathil --- docs/api/enums.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/api/enums.rst b/docs/api/enums.rst index b54f962e47..7cf7526575 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -29,7 +29,12 @@ of :class:`enum.Enum`. A string. .. attribute:: integer - An integer. + An integer between -2⁵³ and 2⁵³. + + .. note:: + + IDs, such as 881224361015672863, are often too big for this input type. + .. attribute:: boolean A boolean. @@ -47,7 +52,11 @@ of :class:`enum.Enum`. A mentionable (user or role). .. attribute:: number - A floating number. + A floating-point number between -2⁵³ and 2⁵³. + + .. note:: + + IDs, such as 881224361015672863, are often too big for this input type. .. attribute:: attachment From abef1ce801a2d195c9133ee9adf28adf2a274c9d Mon Sep 17 00:00:00 2001 From: plun1331 Date: Sat, 29 Jun 2024 15:20:46 -0700 Subject: [PATCH 092/108] feat: update monetization (#2438) --- CHANGELOG.md | 2 + discord/client.py | 64 +++++++++++++++++++++++++--- discord/components.py | 7 ++++ discord/enums.py | 10 +++++ discord/guild.py | 56 +++++++++++++++++++++++++ discord/http.py | 37 ++++++++++++++++ discord/interactions.py | 6 +++ discord/iterators.py | 84 +++++++++++++++++++++++++++++++++++++ discord/monetization.py | 33 ++++++++++++++- discord/types/components.py | 4 +- discord/ui/button.py | 35 +++++++++++++--- discord/user.py | 58 +++++++++++++++++++++++++ docs/api/enums.rst | 43 ++++++++++++++++++- 13 files changed, 426 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32b1ae5f9d..ee0e6755d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2450](https://github.com/Pycord-Development/pycord/pull/2450)) - Added support for user-installable applications. ([#2409](https://github.com/Pycord-Development/pycord/pull/2409) +- Added support for one-time purchases for Discord monetization. + ([#2438](https://github.com/Pycord-Development/pycord/pull/2438)) ### Fixed diff --git a/discord/client.py b/discord/client.py index d46964042f..0044272d59 100644 --- a/discord/client.py +++ b/discord/client.py @@ -49,7 +49,7 @@ from .guild import Guild from .http import HTTPClient from .invite import Invite -from .iterators import GuildIterator +from .iterators import EntitlementIterator, GuildIterator from .mentions import AllowedMentions from .monetization import SKU, Entitlement from .object import Object @@ -2043,17 +2043,71 @@ async def fetch_skus(self) -> list[SKU]: data = await self._connection.http.list_skus(self.application_id) return [SKU(data=s) for s in data] - async def fetch_entitlements(self) -> list[Entitlement]: + async def fetch_entitlements( + self, + user: Snowflake | None = None, + skus: list[Snowflake] | None = None, + before: SnowflakeTime | None = None, + after: SnowflakeTime | None = None, + limit: int | None = 100, + guild: Snowflake | None = None, + exclude_ended: bool = False, + ) -> EntitlementIterator: """|coro| Fetches the bot's entitlements. .. versionadded:: 2.5 + Parameters + ---------- + user: :class:`.abc.Snowflake` | None + Limit the fetched entitlements to entitlements owned by this user. + skus: list[:class:`.abc.Snowflake`] | None + Limit the fetched entitlements to entitlements that are for these SKUs. + before: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None + Retrieves guilds before this date or object. + If a datetime is provided, it is recommended to use a UTC-aware datetime. + If the datetime is naive, it is assumed to be local time. + after: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None + Retrieve guilds after this date or object. + If a datetime is provided, it is recommended to use a UTC-aware datetime. + If the datetime is naive, it is assumed to be local time. + limit: Optional[:class:`int`] + The number of entitlements to retrieve. + If ``None``, retrieves every entitlement, which may be slow. + Defaults to ``100``. + guild: :class:`.abc.Snowflake` | None + Limit the fetched entitlements to entitlements owned by this guild. + exclude_ended: :class:`bool` + Whether to limit the fetched entitlements to those that have not ended. + Defaults to ``False``. + Returns ------- List[:class:`.Entitlement`] - The bot's entitlements. + The application's entitlements. + + Raises + ------ + :exc:`HTTPException` + Retrieving the entitlements failed. + """ + return EntitlementIterator( + self._connection, + user_id=user.id, + sku_ids=[sku.id for sku in skus], + before=before, + after=after, + limit=limit, + guild_id=guild.id, + exclude_ended=exclude_ended, + ) + + @property + def store_url(self) -> str: + """:class:`str`: The URL that leads to the application's store page for monetization. + + .. versionadded:: 2.6 """ - data = await self._connection.http.list_entitlements(self.application_id) - return [Entitlement(data=e, state=self._connection) for e in data] + return f"https://discord.com/application-directory/{self.application_id}/store" diff --git a/discord/components.py b/discord/components.py index eb360f7250..d85fc4b07c 100644 --- a/discord/components.py +++ b/discord/components.py @@ -234,6 +234,8 @@ class Button(Component): The label of the button, if any. emoji: Optional[:class:`PartialEmoji`] The emoji of the button, if available. + sku_id: Optional[:class:`int`] + The ID of the SKU this button refers to. """ __slots__: tuple[str, ...] = ( @@ -243,6 +245,7 @@ class Button(Component): "disabled", "label", "emoji", + "sku_id", ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ @@ -259,6 +262,7 @@ def __init__(self, data: ButtonComponentPayload): self.emoji = PartialEmoji.from_dict(data["emoji"]) except KeyError: self.emoji = None + self.sku_id: str | None = data.get("sku_id") def to_dict(self) -> ButtonComponentPayload: payload = { @@ -276,6 +280,9 @@ def to_dict(self) -> ButtonComponentPayload: if self.emoji: payload["emoji"] = self.emoji.to_dict() + if self.sku_id: + payload["sku_id"] = self.sku_id + return payload # type: ignore diff --git a/discord/enums.py b/discord/enums.py index 1446e1af7d..14aa54d460 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -724,6 +724,7 @@ class ButtonStyle(Enum): success = 3 danger = 4 link = 5 + premium = 6 # Aliases blurple = 1 @@ -1005,6 +1006,8 @@ class ReactionType(Enum): class SKUType(Enum): """The SKU type""" + durable = 2 + consumable = 3 subscription = 5 subscription_group = 6 @@ -1012,6 +1015,13 @@ class SKUType(Enum): class EntitlementType(Enum): """The entitlement type""" + purchase = 1 + premium_subscription = 2 + developer_gift = 3 + test_mode_purchase = 4 + free_purchase = 5 + user_gift = 6 + premium_purchase = 7 application_subscription = 8 diff --git a/discord/guild.py b/discord/guild.py index 15bf1394a3..515d7e43af 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4070,3 +4070,59 @@ async def create_test_entitlement(self, sku: Snowflake) -> Entitlement: } data = await self._state.http.create_test_entitlement(self.id, payload) return Entitlement(data=data, state=self._state) + + async def fetch_entitlements( + self, + skus: list[Snowflake] | None = None, + before: SnowflakeTime | None = None, + after: SnowflakeTime | None = None, + limit: int | None = 100, + exclude_ended: bool = False, + ) -> EntitlementIterator: + """|coro| + + Fetches this guild's entitlements. + + This is identical to :meth:`Client.fetch_entitlements` with the ``guild`` parameter. + + .. versionadded:: 2.6 + + Parameters + ---------- + skus: list[:class:`.abc.Snowflake`] | None + Limit the fetched entitlements to entitlements that are for these SKUs. + before: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None + Retrieves guilds before this date or object. + If a datetime is provided, it is recommended to use a UTC-aware datetime. + If the datetime is naive, it is assumed to be local time. + after: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None + Retrieve guilds after this date or object. + If a datetime is provided, it is recommended to use a UTC-aware datetime. + If the datetime is naive, it is assumed to be local time. + limit: Optional[:class:`int`] + The number of entitlements to retrieve. + If ``None``, retrieves every entitlement, which may be slow. + Defaults to ``100``. + exclude_ended: :class:`bool` + Whether to limit the fetched entitlements to those that have not ended. + Defaults to ``False``. + + Returns + ------- + List[:class:`.Entitlement`] + The application's entitlements. + + Raises + ------ + :exc:`HTTPException` + Retrieving the entitlements failed. + """ + return EntitlementIterator( + self._state, + sku_ids=[sku.id for sku in skus], + before=before, + after=after, + limit=limit, + guild_id=self.id, + exclude_ended=exclude_ended, + ) diff --git a/discord/http.py b/discord/http.py index 85ea7c002c..e3d6e7dcc9 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2957,12 +2957,49 @@ def list_skus( def list_entitlements( self, application_id: Snowflake, + *, + user_id: Snowflake | None = None, + sku_ids: list[Snowflake] | None = None, + before: Snowflake | None = None, + after: Snowflake | None = None, + limit: int | None = None, + guild_id: Snowflake | None = None, + exclude_ended: bool | None = None, ) -> Response[list[monetization.Entitlement]]: + params: dict[str, Any] = {} + if user_id is not None: + params["user_id"] = user_id + if sku_ids is not None: + params["sku_ids"] = ",".join(sku_ids) + if before is not None: + params["before"] = before + if after is not None: + params["after"] = after + if limit is not None: + params["limit"] = limit + if guild_id is not None: + params["guild_id"] = guild_id + if exclude_ended is not None: + params["exclude_ended"] = exclude_ended + r = Route( "GET", "/applications/{application_id}/entitlements", application_id=application_id, ) + return self.request(r, params=params) + + def consume_entitlement( + self, + application_id: Snowflake, + entitlement_id: Snowflake, + ) -> Response[None]: + r = Route( + "POST", + "/applications/{application_id}/entitlements/{entitlement_id}/consume", + application_id=application_id, + entitlement_id=entitlement_id, + ) return self.request(r) def create_test_entitlement( diff --git a/discord/interactions.py b/discord/interactions.py index 5725796d06..ee4df62014 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -1229,10 +1229,16 @@ async def send_modal(self, modal: Modal) -> Interaction: self._parent._state.store_modal(modal, self._parent.user.id) return self._parent + @utils.deprecated("a button with type ButtonType.premium", "2.6") async def premium_required(self) -> Interaction: """|coro| + Responds to this interaction by sending a premium required message. + .. deprecated:: 2.6 + + A button with type :attr:`ButtonType.premium` should be used instead. + Raises ------ HTTPException diff --git a/discord/iterators.py b/discord/iterators.py index 438c41d2ef..2f863ddb9d 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -40,6 +40,7 @@ from .audit_logs import AuditLogEntry from .errors import NoMoreItems +from .monetization import Entitlement from .object import Object from .utils import maybe_coroutine, snowflake_time, time_snowflake @@ -50,6 +51,7 @@ "GuildIterator", "MemberIterator", "ScheduledEventSubscribersIterator", + "EntitlementIterator", ) if TYPE_CHECKING: @@ -964,6 +966,7 @@ def user_from_payload(self, data): async def fill_subs(self): if not self._get_retrieve(): return + before = self.before.id if self.before else None after = self.after.id if self.after else None data = await self.get_subscribers( @@ -988,3 +991,84 @@ async def fill_subs(self): await self.subscribers.put(self.member_from_payload(element)) else: await self.subscribers.put(self.user_from_payload(element)) + + +class EntitlementIterator(_AsyncIterator["Entitlement"]): + def __init__( + self, + state, + user_id: int | None = None, + sku_ids: list[int] | None = None, + before: datetime.datetime | Object | None = None, + after: datetime.datetime | Object | None = None, + limit: int | None = None, + guild_id: int | None = None, + exclude_ended: bool | None = None, + ): + self.user_id = user_id + self.sku_ids = sku_ids + + if isinstance(before, datetime.datetime): + before = Object(id=time_snowflake(before, high=False)) + if isinstance(after, datetime.datetime): + after = Object(id=time_snowflake(after, high=True)) + + self.before = before + self.after = after + self.limit = limit + self.guild_id = guild_id + self.exclude_ended = exclude_ended + + self.state = state + self.get_entitlements = state.http.list_entitlements + self.entitlements = asyncio.Queue() + + async def next(self) -> BanEntry: + if self.entitlements.empty(): + await self.fill_entitlements() + + try: + return self.entitlements.get_nowait() + except asyncio.QueueEmpty: + raise NoMoreItems() + + def _get_retrieve(self): + l = self.limit + if l is None or l > 100: + r = 100 + else: + r = l + self.retrieve = r + return r > 0 + + async def fill_entitlements(self): + if not self._get_retrieve(): + return + + before = self.before.id if self.before else None + after = self.after.id if self.after else None + data = await self.get_entitlements( + self.state.application_id, + before=before, + after=after, + limit=self.retrieve, + user_id=self.user_id, + guild_id=self.guild_id, + sku_ids=self.sku_ids, + exclude_ended=self.exclude_ended, + ) + + if not data: + # no data, terminate + return + + if self.limit: + self.limit -= self.retrieve + + if len(data) < 100: + self.limit = 0 # terminate loop + + self.after = Object(id=int(data[-1]["id"])) + + for element in reversed(data): + await self.entitlements.put(Entitlement(data=element, state=self.state)) diff --git a/discord/monetization.py b/discord/monetization.py index 487c6659e1..f18ce0593b 100644 --- a/discord/monetization.py +++ b/discord/monetization.py @@ -96,6 +96,11 @@ def __str__(self) -> str: def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) and other.id == self.id + @property + def url(self) -> str: + """:class:`str`: Returns the URL for the SKU.""" + return f"https://discord.com/application-directory/{self.application_id}/store/{self.id}" + class Entitlement(Hashable): """Represents a Discord entitlement. @@ -122,6 +127,10 @@ class Entitlement(Hashable): When the entitlement expires. guild_id: Union[:class:`int`, :class:`MISSING`] The ID of the guild that owns this entitlement. + consumed: :class:`bool` + Whether or not this entitlement has been consumed. + This will always be ``False`` for entitlements that are not + of type :attr:`EntitlementType.consumable`. """ __slots__ = ( @@ -135,6 +144,7 @@ class Entitlement(Hashable): "starts_at", "ends_at", "guild_id", + "consumed", ) def __init__(self, *, data: EntitlementPayload, state: ConnectionState) -> None: @@ -150,17 +160,38 @@ def __init__(self, *, data: EntitlementPayload, state: ConnectionState) -> None: ) self.ends_at: datetime | MISSING = parse_time(data.get("ends_at")) or MISSING self.guild_id: int | MISSING = _get_as_snowflake(data, "guild_id") or MISSING + self.consumed: bool = data.get("consumed", False) def __repr__(self) -> str: return ( f"" + f"starts_at={self.starts_at} ends_at={self.ends_at} guild_id={self.guild_id} consumed={self.consumed}>" ) def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) and other.id == self.id + async def consume(self) -> None: + """|coro| + + Consumes this entitlement. + + This can only be done on entitlements of type :attr:`EntitlementType.consumable`. + + Raises + ------ + TypeError + The entitlement is not consumable. + HTTPException + Consuming the entitlement failed. + """ + if self.type is not EntitlementType.consumable: + raise TypeError("Cannot consume non-consumable entitlement") + + await self._state.http.consume_entitlement(self._state.application_id, self.id) + self.consumed = True + async def delete(self) -> None: """|coro| diff --git a/discord/types/components.py b/discord/types/components.py index fa82d47b6f..4c0ce30ad4 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -30,9 +30,10 @@ from .._typed_dict import NotRequired, TypedDict from .channel import ChannelType from .emoji import PartialEmoji +from .snowflake import Snowflake ComponentType = Literal[1, 2, 3, 4] -ButtonStyle = Literal[1, 2, 3, 4, 5] +ButtonStyle = Literal[1, 2, 3, 4, 5, 6] InputTextStyle = Literal[1, 2] @@ -49,6 +50,7 @@ class ButtonComponent(TypedDict): label: NotRequired[str] type: Literal[2] style: ButtonStyle + sku_id: Snowflake class InputText(TypedDict): diff --git a/discord/ui/button.py b/discord/ui/button.py index c607de459c..bdd49d21f8 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -67,6 +67,8 @@ class Button(Item[V]): 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. + sku_id: Optional[Union[:class:`int`]] + The ID of the SKU this button refers to. row: Optional[:class:`int`] The relative row this button belongs to. A Discord component can only have 5 rows. By default, items are arranged automatically into those 5 rows. If you'd @@ -81,6 +83,7 @@ class Button(Item[V]): "disabled", "label", "emoji", + "sku_id", "row", ) @@ -93,6 +96,7 @@ def __init__( custom_id: str | None = None, url: str | None = None, emoji: str | Emoji | PartialEmoji | None = None, + sku_id: int | None = None, row: int | None = None, ): super().__init__() @@ -102,6 +106,10 @@ def __init__( raise ValueError("custom_id must be 100 characters or fewer") if custom_id is not None and url is not None: raise TypeError("cannot mix both url and custom_id with Button") + if sku_id is not None and url is not None: + raise TypeError("cannot mix both url and sku_id with Button") + if custom_id is not None and sku_id is not None: + raise TypeError("cannot mix both sku_id and custom_id with Button") if not isinstance(custom_id, str) and custom_id is not None: raise TypeError( @@ -114,6 +122,8 @@ def __init__( if url is not None: style = ButtonStyle.link + if sku_id is not None: + style = ButtonStyle.premium if emoji is not None: if isinstance(emoji, str): @@ -134,6 +144,7 @@ def __init__( label=label, style=style, emoji=emoji, + sku_id=sku_id, ) self.row = row @@ -212,6 +223,20 @@ def emoji(self, value: str | Emoji | PartialEmoji | None): # type: ignore f" {value.__class__} instead" ) + @property + def sku_id(self) -> int | None: + """The ID of the SKU this button refers to.""" + return self._underlying.sku_id + + @sku_id.setter + def sku_id(self, value: int | None): # type: ignore + if value is None: + self._underlying.sku_id = None + elif isinstance(value, int): + self._underlying.sku_id = value + else: + raise TypeError(f"expected int or None, received {value.__class__} instead") + @classmethod def from_component(cls: type[B], button: ButtonComponent) -> B: return cls( @@ -221,6 +246,7 @@ def from_component(cls: type[B], button: ButtonComponent) -> B: custom_id=button.custom_id, url=button.url, emoji=button.emoji, + sku_id=button.sku_id, row=None, ) @@ -260,11 +286,10 @@ def button( .. note:: - Buttons with a URL cannot be created with this function. - Consider creating a :class:`Button` manually instead. - This is because buttons with a URL do not have a callback - associated with them since Discord does not do any processing - with it. + Premium and link buttons cannot be created with this decorator. Consider + creating a :class:`Button` object manually instead. These types of + buttons do not have a callback associated since Discord doesn't handle + them when clicked. Parameters ---------- diff --git a/discord/user.py b/discord/user.py index 57580575a0..5c2f41feb1 100644 --- a/discord/user.py +++ b/discord/user.py @@ -32,12 +32,14 @@ from .asset import Asset from .colour import Colour from .flags import PublicUserFlags +from .iterators import EntitlementIterator from .monetization import Entitlement from .utils import MISSING, _bytes_to_base64_data, snowflake_time if TYPE_CHECKING: from datetime import datetime + from .abc import Snowflake, SnowflakeTime from .channel import DMChannel from .guild import Guild from .message import Message @@ -636,3 +638,59 @@ async def create_test_entitlement(self, sku: discord.abc.Snowflake) -> Entitleme } data = await self._state.http.create_test_entitlement(self.id, payload) return Entitlement(data=data, state=self._state) + + async def fetch_entitlements( + self, + skus: list[Snowflake] | None = None, + before: SnowflakeTime | None = None, + after: SnowflakeTime | None = None, + limit: int | None = 100, + exclude_ended: bool = False, + ) -> EntitlementIterator: + """|coro| + + Fetches this user's entitlements. + + This is identical to :meth:`Client.fetch_entitlements` with the ``user`` parameter. + + .. versionadded:: 2.6 + + Parameters + ---------- + skus: list[:class:`.abc.Snowflake`] | None + Limit the fetched entitlements to entitlements that are for these SKUs. + before: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None + Retrieves guilds before this date or object. + If a datetime is provided, it is recommended to use a UTC-aware datetime. + If the datetime is naive, it is assumed to be local time. + after: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None + Retrieve guilds after this date or object. + If a datetime is provided, it is recommended to use a UTC-aware datetime. + If the datetime is naive, it is assumed to be local time. + limit: Optional[:class:`int`] + The number of entitlements to retrieve. + If ``None``, retrieves every entitlement, which may be slow. + Defaults to ``100``. + exclude_ended: :class:`bool` + Whether to limit the fetched entitlements to those that have not ended. + Defaults to ``False``. + + Returns + ------- + List[:class:`.Entitlement`] + The application's entitlements. + + Raises + ------ + :exc:`HTTPException` + Retrieving the entitlements failed. + """ + return EntitlementIterator( + self._state, + sku_ids=[sku.id for sku in skus], + before=before, + after=after, + limit=limit, + user_id=self.id, + exclude_ended=exclude_ended, + ) diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 7cf7526575..3f8e8f6e5f 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -527,6 +527,9 @@ of :class:`enum.Enum`. .. attribute:: link Represents a link button. + .. attribute:: premium + + Represents a premium button. .. attribute:: blurple @@ -2378,6 +2381,16 @@ of :class:`enum.Enum`. .. versionadded:: 2.5 + .. attribute:: durable + + A one-time purchase that is permanent and is not subject to either renewal + or consumption, such as lifetime access to an app's premium features. + + .. attribute:: consumable + + A one-time, non-renewable purchase that provides access, such as a temporary + power-up or boost in a game. + .. attribute:: subscription Represents a recurring subscription. @@ -2393,7 +2406,35 @@ of :class:`enum.Enum`. .. versionadded:: 2.5 - .. attribute:: app_subscription + .. attribute:: purchase + + Entitlement was purchased by the user. + + .. attribute:: premium_subscription + + Entitlement is for a Discord Nitro subscription. + + .. attribute:: developer_gift + + Entitlement was gifted by the developer. + + .. attribute:: test_mode_purchase + + Entitlement was purchased by a developer in the application's test mode. + + .. attribute:: free_purchase + + Entitlement was granted when the SKU was free. + + .. attribute:: user_gift + + Entitlement was gifted by another user. + + .. attribute:: premium_purchase + + Entitlement was claimed by a user for free as a Nitro subscriber. + + .. attribute:: application_subscription Entitlement was purchased as an app subscription. From a344a501c8bca68dd6bf3d255e20acf4f0fdc350 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 30 Jun 2024 15:13:32 +0100 Subject: [PATCH 093/108] fix: always cast PartialMessage(able) IDs to int (#2406) * force partial IDs to int * changelog * Update CHANGELOG.md Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> * different fix * adjust changelog --------- Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/channel.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0e6755d2..04e709d410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2390](https://github.com/Pycord-Development/pycord/pull/2390)) - Fixed `NameError` in some instances of `Interaction`. ([#2402](https://github.com/Pycord-Development/pycord/pull/2402)) +- Fixed interactions being ignored due to `PartialMessage.id` being of type `str`. + ([#2406](https://github.com/Pycord-Development/pycord/pull/2406)) - Fixed the type-hinting of `ScheduledEvent.subscribers` to reflect actual behavior. ([#2400](https://github.com/Pycord-Development/pycord/pull/2400)) - Fixed `ScheduledEvent.subscribers` behavior with `limit=None`. diff --git a/discord/channel.py b/discord/channel.py index 77c733194f..7608e1757d 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1339,7 +1339,7 @@ async def create_thread( f.close() ret = Thread(guild=self.guild, state=self._state, data=data) - msg = ret.get_partial_message(data["last_message_id"]) + msg = ret.get_partial_message(int(data["last_message_id"])) if view: state.store_view(view, msg.id) From 267a644aa602c7667be4b9f4c6f4a68e1ee09c48 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 30 Jun 2024 23:50:06 +0100 Subject: [PATCH 094/108] feat: add Attachment.title (#2486) --- CHANGELOG.md | 2 ++ discord/message.py | 7 +++++++ discord/types/message.py | 1 + 3 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e709d410..6c75a9b1e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2409](https://github.com/Pycord-Development/pycord/pull/2409) - Added support for one-time purchases for Discord monetization. ([#2438](https://github.com/Pycord-Development/pycord/pull/2438)) +- Added `Attachment.title`. + ([#2486](https://github.com/Pycord-Development/pycord/pull/2486)) ### Fixed diff --git a/discord/message.py b/discord/message.py index 109bef02c8..23a81bbd96 100644 --- a/discord/message.py +++ b/discord/message.py @@ -155,6 +155,11 @@ class Attachment(Hashable): The attachment's width, in pixels. Only applicable to images and videos. filename: :class:`str` The attachment's filename. + title: Optional[:class:`str`] + The attachment's title. This is equal to the original :attr:`filename` (without an extension) + if special characters were filtered from it. + + .. versionadded:: 2.6 url: :class:`str` The attachment URL. If the message this attachment was attached to is deleted, then this will 404. @@ -213,6 +218,7 @@ class Attachment(Hashable): "_ex", "_is", "hm", + "title", ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState): @@ -221,6 +227,7 @@ def __init__(self, *, data: AttachmentPayload, state: ConnectionState): self.height: int | None = data.get("height") self.width: int | None = data.get("width") self.filename: str = data["filename"] + self.title: str | None = data.get("title") self.url: str = data.get("url") self.proxy_url: str = data.get("proxy_url") self._http = state.http diff --git a/discord/types/message.py b/discord/types/message.py index 8988891efa..a29d1487a4 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -80,6 +80,7 @@ class Attachment(TypedDict): duration_secs: NotRequired[float] waveform: NotRequired[str] flags: NotRequired[int] + title: NotRequired[str] MessageActivityType = Literal[1, 2, 3, 5] From de117fec609b391e28ddd8fba83e4b47569156be Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Mon, 1 Jul 2024 00:18:01 +0100 Subject: [PATCH 095/108] feat: implement MemberFlags (#2489) --- CHANGELOG.md | 3 ++ discord/flags.py | 74 +++++++++++++++++++++++++++++++++++++++ discord/member.py | 31 ++++++++++++++++ discord/types/member.py | 1 + docs/api/data_classes.rst | 5 +++ 5 files changed, 114 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c75a9b1e1..13a0bafdd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ These changes are available on the `master` branch, but have not yet been releas ([#2438](https://github.com/Pycord-Development/pycord/pull/2438)) - Added `Attachment.title`. ([#2486](https://github.com/Pycord-Development/pycord/pull/2486)) +- Added `MemberFlags`. ([#2489](https://github.com/Pycord-Development/pycord/pull/2489)) +- Added `bypass_verification` parameter to `Member.edit`. + ([#2489](https://github.com/Pycord-Development/pycord/pull/2489)) ### Fixed diff --git a/discord/flags.py b/discord/flags.py index 37fd8baa24..df521eca1f 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -39,6 +39,7 @@ "ApplicationFlags", "ChannelFlags", "SKUFlags", + "MemberFlags", ) FV = TypeVar("FV", bound="flag_value") @@ -1684,3 +1685,76 @@ def guild_subscription(self): def user_subscription(self): """:class:`bool`: Returns ``True`` if the SKU is a user subscription.""" return 1 << 8 + + +@fill_with_flags() +class MemberFlags(BaseFlags): + r"""Wraps up the Discord Member flags. + + .. container:: operations + + .. describe:: x == y + + Checks if two MemberFlags are equal. + .. describe:: x != y + + Checks if two MemberFlags are not equal. + .. describe:: x + y + + Adds two flags together. Equivalent to ``x | y``. + .. describe:: x - y + + Subtracts two flags from each other. + .. describe:: x | y + + Returns the union of two flags. Equivalent to ``x + y``. + .. describe:: x & y + + Returns the intersection of two flags. + .. describe:: ~x + + Returns the inverse of a flag. + .. describe:: hash(x) + + Return the flag's hash. + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + .. versionadded:: 2.6 + + Attributes + ----------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + __slots__ = () + + @flag_value + def did_rejoin(self): + """:class:`bool`: Returns ``True`` if the member left and rejoined the guild.""" + return 1 << 0 + + @flag_value + def completed_onboarding(self): + """:class:`bool`: Returns ``True`` if the member has completed onboarding.""" + return 1 << 1 + + @flag_value + def bypasses_verification(self): + """:class:`bool`: Returns ``True`` if the member is exempt from verification requirements. + + .. note:: + + This can be edited through :func:`~discord.Member.edit`. + """ + return 1 << 2 + + @flag_value + def started_onboarding(self): + """:class:`bool`: Returns ``True`` if the member has started onboarding.""" + return 1 << 3 diff --git a/discord/member.py b/discord/member.py index 57c04b2936..18ebf1267f 100644 --- a/discord/member.py +++ b/discord/member.py @@ -39,6 +39,7 @@ from .asset import Asset from .colour import Colour from .enums import Status, try_enum +from .flags import MemberFlags from .object import Object from .permissions import Permissions from .user import BaseUser, User, _UserTag @@ -269,6 +270,10 @@ class Member(discord.abc.Messageable, _UserTag): An aware datetime object that specifies the date and time in UTC when the member will be removed from timeout. .. versionadded:: 2.0 + flags: :class:`MemberFlags` + Extra attributes of the member. + + .. versionadded:: 2.6 """ __slots__ = ( @@ -284,6 +289,7 @@ class Member(discord.abc.Messageable, _UserTag): "_state", "_avatar", "communication_disabled_until", + "flags", ) if TYPE_CHECKING: @@ -325,6 +331,7 @@ def __init__( self.communication_disabled_until: datetime.datetime | None = utils.parse_time( data.get("communication_disabled_until") ) + self.flags: MemberFlags = MemberFlags._from_value(data.get("flags", 0)) def __str__(self) -> str: return str(self._user) @@ -400,6 +407,7 @@ def _copy(cls: type[M], member: M) -> M: self._state = member._state self._avatar = member._avatar self.communication_disabled_until = member.communication_disabled_until + self.flags = member.flags # Reference will not be copied unless necessary by PRESENCE_UPDATE # See below @@ -429,6 +437,7 @@ def _update(self, data: MemberPayload) -> None: self.communication_disabled_until = utils.parse_time( data.get("communication_disabled_until") ) + self.flags = MemberFlags._from_value(data.get("flags", 0)) def _presence_update( self, data: PartialPresenceUpdate, user: UserPayload @@ -729,6 +738,7 @@ async def edit( voice_channel: VocalGuildChannel | None = MISSING, reason: str | None = None, communication_disabled_until: datetime.datetime | None = MISSING, + bypass_verification: bool | None = MISSING, ) -> Member | None: """|coro| @@ -751,6 +761,18 @@ async def edit( +------------------------------+--------------------------------------+ | communication_disabled_until | :attr:`Permissions.moderate_members` | +------------------------------+--------------------------------------+ + | bypass_verification | See note below | + +------------------------------+--------------------------------------+ + + .. note:: + + `bypass_verification` may be edited under three scenarios: + + - Client has :attr:`Permissions.manage_guild` + + - Client has :attr:`Permissions.manage_roles` + + - Client has ALL THREE of :attr:`Permissions.moderate_members`, :attr:`Permissions.kick_members`, and :attr:`Permissions.ban_members` All parameters are optional. @@ -785,6 +807,10 @@ async def edit( from timeout. .. versionadded:: 2.0 + bypass_verification: Optional[:class:`bool`] + Indicates if the member should bypass the guild's verification requirements. + + .. versionadded:: 2.6 Returns ------- @@ -849,6 +875,11 @@ async def edit( else: payload["communication_disabled_until"] = communication_disabled_until + if bypass_verification is not MISSING: + flags = MemberFlags._from_value(self.flags.value) + flags.bypasses_verification = bypass_verification + payload["flags"] = flags.value + if payload: data = await http.edit_member(guild_id, self.id, reason=reason, **payload) return Member(data=data, guild=self.guild, state=self._state) diff --git a/discord/types/member.py b/discord/types/member.py index c6fdf6183a..0bc1071fb1 100644 --- a/discord/types/member.py +++ b/discord/types/member.py @@ -48,6 +48,7 @@ class Member(PartialMember, total=False): pending: bool permissions: str communication_disabled_until: str + flags: int class _OptionalMemberWithUser(PartialMember, total=False): diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst index f0508ac09b..4c4de34e5a 100644 --- a/docs/api/data_classes.rst +++ b/docs/api/data_classes.rst @@ -166,6 +166,11 @@ Flags .. autoclass:: SKUFlags() :members: +.. attributetable:: MemberFlags + +.. autoclass:: MemberFlags() + :members: + Colour ------ From af87860b600c59098175ed6a70060c34b1d92fb1 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:32:24 +0100 Subject: [PATCH 096/108] feat: implement RoleFlags (#2487) * implement roleflags * style(pre-commit): auto fixes from pre-commit.com hooks * cl * style(pre-commit): auto fixes from pre-commit.com hooks * Update discord/flags.py Signed-off-by: plun1331 * Update CHANGELOG.md Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> * fix docs * again? * better formatting * flags.py aktualisieren Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * test formatting fix * resolve conflicts * revert --------- Signed-off-by: plun1331 Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 1 + discord/flags.py | 55 +++++++++++++++++++++++++++++++++++++++ discord/role.py | 8 ++++++ discord/types/role.py | 1 + docs/api/data_classes.rst | 5 ++++ 5 files changed, 70 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a0bafdd3..3bae94f5ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ These changes are available on the `master` branch, but have not yet been releas - Added `MemberFlags`. ([#2489](https://github.com/Pycord-Development/pycord/pull/2489)) - Added `bypass_verification` parameter to `Member.edit`. ([#2489](https://github.com/Pycord-Development/pycord/pull/2489)) +- Added `RoleFlags`. ([#2487](https://github.com/Pycord-Development/pycord/pull/2487)) ### Fixed diff --git a/discord/flags.py b/discord/flags.py index df521eca1f..8a1ac8fd7e 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -39,6 +39,7 @@ "ApplicationFlags", "ChannelFlags", "SKUFlags", + "RoleFlags", "MemberFlags", ) @@ -1758,3 +1759,57 @@ def bypasses_verification(self): def started_onboarding(self): """:class:`bool`: Returns ``True`` if the member has started onboarding.""" return 1 << 3 + + +@fill_with_flags() +class RoleFlags(BaseFlags): + r"""Wraps up the Discord Role flags. + + .. container:: operations + + .. describe:: x == y + + Checks if two RoleFlags are equal. + .. describe:: x != y + + Checks if two RoleFlags are not equal. + .. describe:: x + y + + Adds two flags together. Equivalent to ``x | y``. + .. describe:: x - y + + Subtracts two flags from each other. + .. describe:: x | y + + Returns the union of two flags. Equivalent to ``x + y``. + .. describe:: x & y + + Returns the intersection of two flags. + .. describe:: ~x + + Returns the inverse of a flag. + .. describe:: hash(x) + + Return the flag's hash. + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + .. versionadded:: 2.6 + + Attributes + ----------- + value: :class:`int` + The raw value. This value is a bit array field of a 53-bit integer + representing the currently available flags. You should query + flags via the properties rather than using this raw value. + """ + + __slots__ = () + + @flag_value + def in_prompt(self): + """:class:`bool`: Returns ``True`` if the role is selectable in one of the guild's :class:`~discord.OnboardingPrompt`.""" + return 1 << 0 diff --git a/discord/role.py b/discord/role.py index 4012d29cb5..1907606e9d 100644 --- a/discord/role.py +++ b/discord/role.py @@ -30,6 +30,7 @@ from .asset import Asset from .colour import Colour from .errors import InvalidArgument +from .flags import RoleFlags from .mixins import Hashable from .permissions import Permissions from .utils import MISSING, _bytes_to_base64_data, _get_as_snowflake, snowflake_time @@ -177,6 +178,11 @@ class Role(Hashable): Only available to guilds that contain ``ROLE_ICONS`` in :attr:`Guild.features`. .. versionadded:: 2.0 + + flags: :class:`RoleFlags` + Extra attributes of the role. + + .. versionadded:: 2.6 """ __slots__ = ( @@ -193,6 +199,7 @@ class Role(Hashable): "unicode_emoji", "_icon", "_state", + "flags", ) def __init__(self, *, guild: Guild, state: ConnectionState, data: RolePayload): @@ -253,6 +260,7 @@ def _update(self, data: RolePayload): self.mentionable: bool = data.get("mentionable", False) self._icon: str | None = data.get("icon") self.unicode_emoji: str | None = data.get("unicode_emoji") + self.flags: RoleFlags = RoleFlags._from_value(data.get("flags", 0)) self.tags: RoleTags | None try: diff --git a/discord/types/role.py b/discord/types/role.py index 8996e7e21d..d387d38b77 100644 --- a/discord/types/role.py +++ b/discord/types/role.py @@ -39,6 +39,7 @@ class Role(TypedDict): permissions: str managed: bool mentionable: bool + flags: int class RoleTags(TypedDict, total=False): diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst index 4c4de34e5a..272b21e74b 100644 --- a/docs/api/data_classes.rst +++ b/docs/api/data_classes.rst @@ -171,6 +171,11 @@ Flags .. autoclass:: MemberFlags() :members: +.. attributetable:: RoleFlags + +.. autoclass:: RoleFlags() + :members: + Colour ------ From 99b40e2e59a7e495c90681b4cafb2e13307b63b8 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:36:20 +0100 Subject: [PATCH 097/108] fix: enum option issues (#2463) * patch enums * cl * document enum choice/autocomplete behaviour * Update discord/commands/options.py Co-authored-by: Icebluewolf <44532864+Icebluewolf@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> * clarify docstring and warn * style(pre-commit): auto fixes from pre-commit.com hooks * import * style(pre-commit): auto fixes from pre-commit.com hooks --------- Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> Co-authored-by: Icebluewolf <44532864+Icebluewolf@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil --- CHANGELOG.md | 2 ++ discord/commands/options.py | 36 +++++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bae94f5ae..09d02c6438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2448](https://github.com/Pycord-Development/pycord/pull/2448)) - Fixed missing `application_id` in `Entitlement.delete`. ([#2458](https://github.com/Pycord-Development/pycord/pull/2458)) +- Fixed issues with enums as `Option` types with long descriptions or too many values + ([#2463](https://github.com/Pycord-Development/pycord/pull/2463)) - Fixed many inaccurate type hints throughout the library. ([#2457](https://github.com/Pycord-Development/pycord/pull/2457)) - Fixed `AttributeError` due to `discord.Option` being initialised with `input_type` set diff --git a/discord/commands/options.py b/discord/commands/options.py index 2ca8c5a39a..9c56e3aeee 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -25,6 +25,7 @@ from __future__ import annotations import inspect +import logging from enum import Enum from typing import TYPE_CHECKING, Literal, Optional, Type, Union @@ -41,7 +42,7 @@ from ..enums import ChannelType from ..enums import Enum as DiscordEnum from ..enums import SlashCommandOptionType -from ..utils import MISSING +from ..utils import MISSING, basic_autocomplete if TYPE_CHECKING: from ..ext.commands import Converter @@ -86,6 +87,8 @@ DMChannel: ChannelType.private, } +_log = logging.getLogger(__name__) + class ThreadOption: """Represents a class that can be passed as the ``input_type`` for an :class:`Option` class. @@ -115,12 +118,13 @@ class Option: input_type: Union[Type[:class:`str`], Type[:class:`bool`], Type[:class:`int`], Type[:class:`float`], Type[:class:`.abc.GuildChannel`], Type[:class:`Thread`], Type[:class:`Member`], Type[:class:`User`], Type[:class:`Attachment`], Type[:class:`Role`], Type[:class:`.abc.Mentionable`], :class:`SlashCommandOptionType`, Type[:class:`.ext.commands.Converter`], Type[:class:`enums.Enum`], Type[:class:`Enum`]] The type of input that is expected for this option. This can be a :class:`SlashCommandOptionType`, an associated class, a channel type, a :class:`Converter`, a converter class or an :class:`enum.Enum`. + If a :class:`enum.Enum` is used and it has up to 25 values, :attr:`choices` will be automatically filled. If the :class:`enum.Enum` has more than 25 values, :attr:`autocomplete` will be implemented with :func:`discord.utils.basic_autocomplete` instead. name: :class:`str` The name of this option visible in the UI. Inherits from the variable name if not provided as a parameter. description: Optional[:class:`str`] The description of this option. - Must be 100 characters or fewer. + Must be 100 characters or fewer. If :attr:`input_type` is a :class:`enum.Enum` and :attr:`description` is not specified, :attr:`input_type`'s docstring will be used. choices: Optional[List[Union[:class:`Any`, :class:`OptionChoice`]]] The list of available choices for this option. Can be a list of values or :class:`OptionChoice` objects (which represent a name:value pair). @@ -195,7 +199,14 @@ def __init__( input_type_is_class = isinstance(input_type, type) if input_type_is_class and issubclass(input_type, (Enum, DiscordEnum)): if description is None: - description = inspect.getdoc(input_type) + description = inspect.cleandoc(input_type.__doc__) + if description and len(description) > 100: + description = description[:97] + "..." + _log.warning( + "Option %s's description was truncated due to Enum %s's docstring exceeding 100 characters.", + self.name, + 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): @@ -250,10 +261,19 @@ def __init__( 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", []) - ] + + self.autocomplete = kwargs.pop("autocomplete", None) + if len(enum_choices) > 25: + self.choices: list[OptionChoice] = [] + for e in enum_choices: + e.value = str(e.value) + self.autocomplete = basic_autocomplete(enum_choices) + self.input_type = SlashCommandOptionType.string + else: + self.choices: list[OptionChoice] = enum_choices or [ + o if isinstance(o, OptionChoice) else OptionChoice(o) + for o in kwargs.pop("choices", []) + ] if self.input_type == SlashCommandOptionType.integer: minmax_types = (int, type(None)) @@ -323,8 +343,6 @@ def __init__( if self.max_length < 1 or self.max_length > 6000: raise AttributeError("max_length must between 1 and 6000 (inclusive)") - self.autocomplete = kwargs.pop("autocomplete", None) - self.name_localizations = kwargs.pop("name_localizations", MISSING) self.description_localizations = kwargs.pop( "description_localizations", MISSING From 02e9d96184b363246b5033a9b1419bd6711b3ae6 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Mon, 1 Jul 2024 13:01:44 +0100 Subject: [PATCH 098/108] feat: implement MessageCall (#2488) * implement messagecalls * style(pre-commit): auto fixes from pre-commit.com hooks * adjust design + cl * style(pre-commit): auto fixes from pre-commit.com hooks * __all__ * message.py aktualisieren Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --------- Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- CHANGELOG.md | 2 ++ discord/message.py | 43 +++++++++++++++++++++++++++++++++++++++ discord/types/message.py | 5 +++++ docs/api/data_classes.rst | 5 +++++ 4 files changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09d02c6438..911ebd6480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ These changes are available on the `master` branch, but have not yet been releas - Added `bypass_verification` parameter to `Member.edit`. ([#2489](https://github.com/Pycord-Development/pycord/pull/2489)) - Added `RoleFlags`. ([#2487](https://github.com/Pycord-Development/pycord/pull/2487)) +- Added `MessageCall` information. + ([#2488](https://github.com/Pycord-Development/pycord/pull/2488)) ### Fixed diff --git a/discord/message.py b/discord/message.py index 23a81bbd96..37ffd86f53 100644 --- a/discord/message.py +++ b/discord/message.py @@ -52,6 +52,7 @@ from .guild import Guild from .member import Member from .mixins import Hashable +from .object import Object from .partial_emoji import PartialEmoji from .poll import Poll from .reaction import Reaction @@ -80,9 +81,11 @@ from .types.message import Message as MessagePayload from .types.message import MessageActivity as MessageActivityPayload from .types.message import MessageApplication as MessageApplicationPayload + from .types.message import MessageCall as MessageCallPayload from .types.message import MessageReference as MessageReferencePayload from .types.message import Reaction as ReactionPayload from .types.poll import Poll as PollPayload + from .types.snowflake import SnowflakeList from .types.threads import ThreadArchiveDuration from .types.user import User as UserPayload from .ui.view import View @@ -96,6 +99,7 @@ "Message", "PartialMessage", "MessageReference", + "MessageCall", "DeletedReferencedMessage", ) @@ -600,6 +604,34 @@ def to_dict(self) -> MessageReferencePayload: to_message_reference_dict = to_dict +class MessageCall: + """Represents information about a call in a private channel. + + .. versionadded:: 2.6 + """ + + def __init__(self, state: ConnectionState, data: MessageCallPayload): + self._state: ConnectionState = state + self._participants: SnowflakeList = data.get("participants", []) + self._ended_timestamp: datetime.datetime | None = utils.parse_time( + data["ended_timestamp"] + ) + + @property + def participants(self) -> list[User | Object]: + """A list of :class:`User` that participated in this call. + + If a user is not found in the client's cache, + then it will be returned as an :class:`Object`. + """ + return [self._state.get_user(int(i)) or Object(i) for i in self._participants] + + @property + def ended_at(self) -> datetime.datetime | None: + """An aware timestamp of when the call ended.""" + return self._ended_timestamp + + def flatten_handlers(cls): prefix = len("_handle_") handlers = [ @@ -747,6 +779,10 @@ class Message(Hashable): poll: Optional[:class:`Poll`] The poll associated with this message, if applicable. + .. versionadded:: 2.6 + call: Optional[:class:`MessageCall`] + The call information associated with this message, if applicable. + .. versionadded:: 2.6 """ @@ -785,6 +821,7 @@ class Message(Hashable): "interaction_metadata", "thread", "_poll", + "call", ) if TYPE_CHECKING: @@ -895,6 +932,12 @@ def __init__( except KeyError: self.thread = None + self.call: MessageCall | None + try: + self.call = MessageCall(state=self._state, data=data["call"]) + except KeyError: + self.call = None + for handler in ("author", "member", "mentions", "mention_roles"): try: getattr(self, f"_handle_{handler}")(data[handler]) diff --git a/discord/types/message.py b/discord/types/message.py index a29d1487a4..f138609d1b 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -154,3 +154,8 @@ class AllowedMentions(TypedDict): roles: SnowflakeList users: SnowflakeList replied_user: bool + + +class MessageCall(TypedDict): + participants: SnowflakeList + ended_timestamp: NotRequired[str] diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst index 272b21e74b..1d891b90cd 100644 --- a/docs/api/data_classes.rst +++ b/docs/api/data_classes.rst @@ -49,6 +49,11 @@ Message .. autoclass:: MessageReference :members: +.. attributetable:: MessageCall + +.. autoclass:: MessageCall + :members: + .. attributetable:: PartialMessage .. autoclass:: PartialMessage From 6bc8309b03ee27ba215f91ab66922809d803149e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 13:16:47 +0000 Subject: [PATCH 099/108] chore(deps-dev): update pylint requirement from ~=3.2.4 to ~=3.2.5 (#2484) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.4...v3.2.5) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index ee9e9cd99c..9fc5d19119 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r _.txt -pylint~=3.2.4 +pylint~=3.2.5 pytest~=8.2.2 pytest-asyncio~=0.23.3 # pytest-order~=1.0.1 From 109a294a85820cd3113cb28e3981e0503925f5e9 Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Tue, 2 Jul 2024 00:55:34 +0300 Subject: [PATCH 100/108] fix: remove_application_command missing case (#2480) * fix: remove_application_command has wrong logic * changelog --- CHANGELOG.md | 2 ++ discord/bot.py | 21 ++++++++++----------- discord/commands/core.py | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 911ebd6480..3bf7cd67cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2457](https://github.com/Pycord-Development/pycord/pull/2457)) - Fixed `AttributeError` due to `discord.Option` being initialised with `input_type` set to `None`. ([#2464](https://github.com/Pycord-Development/pycord/pull/2464)) +- Fixed `remove_application_command` causing issues while reloading extensions. + ([#2480](https://github.com/Pycord-Development/pycord/pull/2480)) ### Changed diff --git a/discord/bot.py b/discord/bot.py index 19a1ca9bdc..a81825b494 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -108,7 +108,7 @@ def application_commands(self) -> list[ApplicationCommand]: return list(self._application_commands.values()) def add_application_command(self, command: ApplicationCommand) -> None: - """Adds a :class:`.ApplicationCommand` into the internal list of commands. + """Adds an :class:`.ApplicationCommand` into the internal list of commands. This is usually not called, instead the :meth:`command` or other shortcut decorators are used instead. @@ -143,7 +143,7 @@ def add_application_command(self, command: ApplicationCommand) -> None: def remove_application_command( self, command: ApplicationCommand ) -> ApplicationCommand | None: - """Remove a :class:`.ApplicationCommand` from the internal list + """Remove an :class:`.ApplicationCommand` from the internal list of commands. .. versionadded:: 2.0 @@ -156,16 +156,15 @@ def remove_application_command( Returns ------- Optional[:class:`.ApplicationCommand`] - The command that was removed. If the name is not valid then + The command that was removed. If the command has not been added, ``None`` is returned instead. """ - if command.id is None: - try: - index = self._pending_application_commands.index(command) - except ValueError: - return None - return self._pending_application_commands.pop(index) - return self._application_commands.pop(command.id, None) + if command.id: + self._application_commands.pop(command.id, None) + + if command in self._pending_application_commands: + self._pending_application_commands.remove(command) + return command @property def get_command(self): @@ -185,7 +184,7 @@ def get_application_command( guild_ids: list[int] | None = None, type: type[ApplicationCommand] = ApplicationCommand, ) -> ApplicationCommand | None: - """Get a :class:`.ApplicationCommand` from the internal list + """Get an :class:`.ApplicationCommand` from the internal list of commands. .. versionadded:: 2.0 diff --git a/discord/commands/core.py b/discord/commands/core.py index d89ea0a5b2..3c886f2dfc 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -519,7 +519,7 @@ def before_invoke(self, coro): called. This makes it a useful function to set up database connections or any type of set up required. - This pre-invoke hook takes a sole parameter, a :class:`.ApplicationContext`. + This pre-invoke hook takes a sole parameter, an :class:`.ApplicationContext`. See :meth:`.Bot.before_invoke` for more info. Parameters @@ -544,7 +544,7 @@ def after_invoke(self, coro): called. This makes it a useful function to clean-up database connections or any type of clean up required. - This post-invoke hook takes a sole parameter, a :class:`.ApplicationContext`. + This post-invoke hook takes a sole parameter, an :class:`.ApplicationContext`. See :meth:`.Bot.after_invoke` for more info. Parameters From ec3cf1652e574d4b59b52bde897a21ef51f221ce Mon Sep 17 00:00:00 2001 From: Timo <35654063+tibue99@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:37:57 +0200 Subject: [PATCH 101/108] fix: premium buttons and entitlement iterator (#2490) * Fix premium buttons * Fix EntitlementIterator call * Test yarl bool * Add TODO * style(pre-commit): auto fixes from pre-commit.com hooks * Cast `exclude_ended` to int * Fix cast * Adjust fetch_entitlements to match other iterators * Add changelog --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++-- discord/client.py | 34 +++++++++++++++++++++++----------- discord/guild.py | 23 +++++++++++++---------- discord/http.py | 2 +- discord/ui/button.py | 2 +- discord/user.py | 16 +++++++--------- 6 files changed, 50 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf7cd67cb..6ec3411622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ These changes are available on the `master` branch, but have not yet been releas - Added `stacklevel` param to `utils.warn_deprecated` and `utils.deprecated`. ([#2450](https://github.com/Pycord-Development/pycord/pull/2450)) - Added support for user-installable applications. - ([#2409](https://github.com/Pycord-Development/pycord/pull/2409) + ([#2409](https://github.com/Pycord-Development/pycord/pull/2409)) - Added support for one-time purchases for Discord monetization. ([#2438](https://github.com/Pycord-Development/pycord/pull/2438)) - Added `Attachment.title`. @@ -93,7 +93,10 @@ These changes are available on the `master` branch, but have not yet been releas `ApplicationCommand.contexts`. ([#2409](https://github.com/Pycord-Development/pycord/pull/2409)) - `Message.interaction` is now deprecated in favor of `Message.interaction_metadata`. - ([#2409](https://github.com/Pycord-Development/pycord/pull/2409) + ([#2409](https://github.com/Pycord-Development/pycord/pull/2409)) +- Replaced `Client.fetch_entitlements` with `Client.entitlements`, which returns an + `EntitlementIterator`. + ([#2490](https://github.com/Pycord-Development/pycord/pull/2490)) ### Removed diff --git a/discord/client.py b/discord/client.py index 0044272d59..4cfaef59cf 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2043,7 +2043,7 @@ async def fetch_skus(self) -> list[SKU]: data = await self._connection.http.list_skus(self.application_id) return [SKU(data=s) for s in data] - async def fetch_entitlements( + def entitlements( self, user: Snowflake | None = None, skus: list[Snowflake] | None = None, @@ -2053,11 +2053,9 @@ async def fetch_entitlements( guild: Snowflake | None = None, exclude_ended: bool = False, ) -> EntitlementIterator: - """|coro| - - Fetches the bot's entitlements. + """Returns an :class:`.AsyncIterator` that enables fetching the application's entitlements. - .. versionadded:: 2.5 + .. versionadded:: 2.6 Parameters ---------- @@ -2083,24 +2081,38 @@ async def fetch_entitlements( Whether to limit the fetched entitlements to those that have not ended. Defaults to ``False``. - Returns - ------- - List[:class:`.Entitlement`] + Yields + ------ + :class:`.Entitlement` The application's entitlements. Raises ------ :exc:`HTTPException` Retrieving the entitlements failed. + + Examples + -------- + + Usage :: + + async for entitlement in client.entitlements(): + print(entitlement.user_id) + + Flattening into a list :: + + entitlements = await user.entitlements().flatten() + + All parameters are optional. """ return EntitlementIterator( self._connection, - user_id=user.id, - sku_ids=[sku.id for sku in skus], + user_id=user.id if user else None, + sku_ids=[sku.id for sku in skus] if skus else None, before=before, after=after, limit=limit, - guild_id=guild.id, + guild_id=guild.id if guild else None, exclude_ended=exclude_ended, ) diff --git a/discord/guild.py b/discord/guild.py index 515d7e43af..f1bbe14aa2 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -68,7 +68,12 @@ from .flags import SystemChannelFlags from .integrations import Integration, _integration_factory from .invite import Invite -from .iterators import AuditLogIterator, BanIterator, MemberIterator +from .iterators import ( + AuditLogIterator, + BanIterator, + EntitlementIterator, + MemberIterator, +) from .member import Member, VoiceState from .mixins import Hashable from .monetization import Entitlement @@ -4071,7 +4076,7 @@ async def create_test_entitlement(self, sku: Snowflake) -> Entitlement: data = await self._state.http.create_test_entitlement(self.id, payload) return Entitlement(data=data, state=self._state) - async def fetch_entitlements( + def entitlements( self, skus: list[Snowflake] | None = None, before: SnowflakeTime | None = None, @@ -4079,11 +4084,9 @@ async def fetch_entitlements( limit: int | None = 100, exclude_ended: bool = False, ) -> EntitlementIterator: - """|coro| + """Returns an :class:`.AsyncIterator` that enables fetching the guild's entitlements. - Fetches this guild's entitlements. - - This is identical to :meth:`Client.fetch_entitlements` with the ``guild`` parameter. + This is identical to :meth:`Client.entitlements` with the ``guild`` parameter. .. versionadded:: 2.6 @@ -4107,9 +4110,9 @@ async def fetch_entitlements( Whether to limit the fetched entitlements to those that have not ended. Defaults to ``False``. - Returns - ------- - List[:class:`.Entitlement`] + Yields + ------ + :class:`.Entitlement` The application's entitlements. Raises @@ -4119,7 +4122,7 @@ async def fetch_entitlements( """ return EntitlementIterator( self._state, - sku_ids=[sku.id for sku in skus], + sku_ids=[sku.id for sku in skus] if skus else None, before=before, after=after, limit=limit, diff --git a/discord/http.py b/discord/http.py index e3d6e7dcc9..b29b35d4b2 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2980,7 +2980,7 @@ def list_entitlements( if guild_id is not None: params["guild_id"] = guild_id if exclude_ended is not None: - params["exclude_ended"] = exclude_ended + params["exclude_ended"] = int(exclude_ended) r = Route( "GET", diff --git a/discord/ui/button.py b/discord/ui/button.py index bdd49d21f8..5487fd6b73 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -117,7 +117,7 @@ def __init__( ) self._provided_custom_id = custom_id is not None - if url is None and custom_id is None: + if url is None and custom_id is None and sku_id is None: custom_id = os.urandom(16).hex() if url is not None: diff --git a/discord/user.py b/discord/user.py index 5c2f41feb1..d986a018f5 100644 --- a/discord/user.py +++ b/discord/user.py @@ -639,7 +639,7 @@ async def create_test_entitlement(self, sku: discord.abc.Snowflake) -> Entitleme data = await self._state.http.create_test_entitlement(self.id, payload) return Entitlement(data=data, state=self._state) - async def fetch_entitlements( + def entitlements( self, skus: list[Snowflake] | None = None, before: SnowflakeTime | None = None, @@ -647,11 +647,9 @@ async def fetch_entitlements( limit: int | None = 100, exclude_ended: bool = False, ) -> EntitlementIterator: - """|coro| - - Fetches this user's entitlements. + """Returns an :class:`.AsyncIterator` that enables fetching the user's entitlements. - This is identical to :meth:`Client.fetch_entitlements` with the ``user`` parameter. + This is identical to :meth:`Client.entitlements` with the ``user`` parameter. .. versionadded:: 2.6 @@ -675,9 +673,9 @@ async def fetch_entitlements( Whether to limit the fetched entitlements to those that have not ended. Defaults to ``False``. - Returns - ------- - List[:class:`.Entitlement`] + Yields + ------ + :class:`.Entitlement` The application's entitlements. Raises @@ -687,7 +685,7 @@ async def fetch_entitlements( """ return EntitlementIterator( self._state, - sku_ids=[sku.id for sku in skus], + sku_ids=[sku.id for sku in skus] if skus else None, before=before, after=after, limit=limit, From d1702cf316feabfcabdf1c4c59e4358f07fd2a78 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Fri, 5 Jul 2024 20:31:30 +0200 Subject: [PATCH 102/108] fix: update audit log filtering & sorting (#2371) * fix: either i'm high and it won't work, or i'm god and this works * Update iterators.py * Update iterators.py Signed-off-by: Lala Sabathil * chore: clean code Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * chore: add changelog entry Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * chore: add changelog entry Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * Update guild.py Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --------- Signed-off-by: Lala Sabathil Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 5 ++++ discord/guild.py | 8 +++--- discord/iterators.py | 60 ++++++++------------------------------------ 3 files changed, 19 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec3411622..d148e47968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,8 @@ These changes are available on the `master` branch, but have not yet been releas to `None`. ([#2464](https://github.com/Pycord-Development/pycord/pull/2464)) - Fixed `remove_application_command` causing issues while reloading extensions. ([#2480](https://github.com/Pycord-Development/pycord/pull/2480)) +- Fixed outdated logic for filtering and sorting audit log entries. + ([#2371](https://github.com/Pycord-Development/pycord/pull/2371)) ### Changed @@ -103,6 +105,9 @@ These changes are available on the `master` branch, but have not yet been releas - Removed the `delete_message_days` parameter from ban methods. Please use `delete_message_seconds` instead. ([#2421](https://github.com/Pycord-Development/pycord/pull/2421)) +- Removed the `oldest_first` parameter from `Guild.audit_logs` in favor of the `before` + and `after` parameters. + ([#2371](https://github.com/Pycord-Development/pycord/pull/2371)) ## [2.5.0] - 2024-03-02 diff --git a/discord/guild.py b/discord/guild.py index f1bbe14aa2..b92ffefb19 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3266,7 +3266,6 @@ def audit_logs( limit: int | None = 100, before: SnowflakeTime | None = None, after: SnowflakeTime | None = None, - oldest_first: bool | None = None, user: Snowflake = None, action: AuditLogAction = None, ) -> AuditLogIterator: @@ -3274,6 +3273,9 @@ def audit_logs( You must have the :attr:`~Permissions.view_audit_log` permission to use this. + See `API documentation `_ + for more information about the `before` and `after` parameters. + Parameters ---------- limit: Optional[:class:`int`] @@ -3286,9 +3288,6 @@ def audit_logs( Retrieve entries after this date or entry. If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time. - oldest_first: :class:`bool` - If set to ``True``, return entries in oldest->newest order. Defaults to ``True`` if - ``after`` is specified, otherwise ``False``. user: :class:`abc.Snowflake` The moderator to filter entries from. action: :class:`AuditLogAction` @@ -3333,7 +3332,6 @@ def audit_logs( before=before, after=after, limit=limit, - oldest_first=oldest_first, user_id=user_id, action_type=action, ) diff --git a/discord/iterators.py b/discord/iterators.py index 2f863ddb9d..b43dd53654 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -476,7 +476,6 @@ def __init__( limit=None, before=None, after=None, - oldest_first=None, user_id=None, action_type=None, ): @@ -485,7 +484,6 @@ def __init__( if isinstance(after, datetime.datetime): after = Object(id=time_snowflake(after, high=True)) - self.reverse = after is not None if oldest_first is None else oldest_first self.guild = guild self.loop = guild._state.loop self.request = guild._state.http.get_audit_logs @@ -496,51 +494,28 @@ def __init__( self.after = after or OLDEST_OBJECT self._users = {} self._state = guild._state - - self._filter = None # entry dict -> bool - self.entries = asyncio.Queue() - if self.reverse: - self._strategy = self._after_strategy - if self.before: - self._filter = lambda m: int(m["id"]) < self.before.id - else: - self._strategy = self._before_strategy - if self.after and self.after != OLDEST_OBJECT: - self._filter = lambda m: int(m["id"]) > self.after.id - - async def _before_strategy(self, retrieve): + async def _retrieve_entries(self, retrieve): before = self.before.id if self.before else None - data: AuditLogPayload = await self.request( - self.guild.id, - limit=retrieve, - user_id=self.user_id, - action_type=self.action_type, - before=before, - ) - - entries = data.get("audit_log_entries", []) - if len(data) and entries: - if self.limit is not None: - self.limit -= retrieve - self.before = Object(id=int(entries[-1]["id"])) - return data.get("users", []), entries - - async def _after_strategy(self, retrieve): after = self.after.id if self.after else None data: AuditLogPayload = await self.request( self.guild.id, limit=retrieve, user_id=self.user_id, action_type=self.action_type, + before=before, after=after, ) + entries = data.get("audit_log_entries", []) if len(data) and entries: if self.limit is not None: self.limit -= retrieve - self.after = Object(id=int(entries[0]["id"])) + if self.before or not self.after: + self.before = Object(id=int(entries[-1]["id"])) + if self.after or not self.before: + self.after = Object(id=int(entries[0]["id"])) return data.get("users", []), entries async def next(self) -> AuditLogEntry: @@ -553,36 +528,23 @@ async def next(self) -> AuditLogEntry: raise NoMoreItems() def _get_retrieve(self): - l = self.limit - if l is None or l > 100: - r = 100 - else: - r = l - self.retrieve = r - return r > 0 + limit = self.limit or 100 + self.retrieve = min(limit, 100) + return self.retrieve > 0 async def _fill(self): from .user import User if self._get_retrieve(): - users, data = await self._strategy(self.retrieve) + users, data = await self._retrieve_entries(self.retrieve) if len(data) < 100: self.limit = 0 # terminate the infinite loop - if self.reverse: - data = reversed(data) - if self._filter: - data = filter(self._filter, data) - for user in users: u = User(data=user, state=self._state) self._users[u.id] = u for element in data: - # TODO: remove this if statement later - if element["action_type"] is None: - continue - await self.entries.put( AuditLogEntry(data=element, users=self._users, guild=self.guild) ) From 73e4e37c73f7e9f2b398dfbe2a5d7ff5ad078f75 Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Sun, 7 Jul 2024 03:11:30 +0300 Subject: [PATCH 103/108] chore: remove `vanity_code` from `Guild.edit` (#2491) chore: remove vanity_code from Guild.edit --- CHANGELOG.md | 2 ++ discord/guild.py | 6 ------ discord/http.py | 10 ---------- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d148e47968..9f12f67931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,8 @@ These changes are available on the `master` branch, but have not yet been releas - Removed the `oldest_first` parameter from `Guild.audit_logs` in favor of the `before` and `after` parameters. ([#2371](https://github.com/Pycord-Development/pycord/pull/2371)) +- Removed the `vanity_code` parameter from `Guild.edit`. + ([#2480](https://github.com/Pycord-Development/pycord/pull/2480)) ## [2.5.0] - 2024-03-02 diff --git a/discord/guild.py b/discord/guild.py index b92ffefb19..eda67a2e13 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1654,7 +1654,6 @@ async def edit( default_notifications: NotificationLevel = MISSING, verification_level: VerificationLevel = MISSING, explicit_content_filter: ContentFilter = MISSING, - vanity_code: str = MISSING, system_channel: TextChannel | None = MISSING, system_channel_flags: SystemChannelFlags = MISSING, preferred_locale: str = MISSING, @@ -1720,8 +1719,6 @@ async def edit( The new default notification level for the guild. explicit_content_filter: :class:`ContentFilter` The new explicit content filter for the guild. - vanity_code: :class:`str` - The new vanity code for the guild. system_channel: Optional[:class:`TextChannel`] The new channel that is used for the system channel. Could be ``None`` for no system channel. system_channel_flags: :class:`SystemChannelFlags` @@ -1764,9 +1761,6 @@ async def edit( http = self._state.http - if vanity_code is not MISSING: - await http.change_vanity_code(self.id, vanity_code, reason=reason) - fields: dict[str, Any] = {} if name is not MISSING: fields["name"] = name diff --git a/discord/http.py b/discord/http.py index b29b35d4b2..65c1dab0eb 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1628,16 +1628,6 @@ def get_vanity_code(self, guild_id: Snowflake) -> Response[invite.VanityInvite]: Route("GET", "/guilds/{guild_id}/vanity-url", guild_id=guild_id) ) - def change_vanity_code( - self, guild_id: Snowflake, code: str, *, reason: str | None = None - ) -> Response[None]: - payload: dict[str, Any] = {"code": code} - return self.request( - Route("PATCH", "/guilds/{guild_id}/vanity-url", guild_id=guild_id), - json=payload, - reason=reason, - ) - def get_all_guild_channels( self, guild_id: Snowflake ) -> Response[list[guild.GuildChannel]]: From e25527fb1cfaf075ab6e58f4ed63d6260704fe47 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:49:22 +0100 Subject: [PATCH 104/108] fix: further address audit log issues (#2492) --- CHANGELOG.md | 2 ++ discord/http.py | 4 ++-- discord/iterators.py | 19 +++++++++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f12f67931..4c996d6e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2480](https://github.com/Pycord-Development/pycord/pull/2480)) - Fixed outdated logic for filtering and sorting audit log entries. ([#2371](https://github.com/Pycord-Development/pycord/pull/2371)) +- Further fixed logic when fetching audit logs. + ([#2492](https://github.com/Pycord-Development/pycord/pull/2492)) ### Changed diff --git a/discord/http.py b/discord/http.py index 65c1dab0eb..8b3db319e7 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1946,9 +1946,9 @@ def get_audit_logs( action_type: AuditLogAction | None = None, ) -> Response[audit_log.AuditLog]: params: dict[str, Any] = {"limit": limit} - if before: + if before is not None: params["before"] = before - if after: + if after is not None: params["after"] = after if user_id: params["user_id"] = user_id diff --git a/discord/iterators.py b/discord/iterators.py index b43dd53654..7404e790a4 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -491,12 +491,14 @@ def __init__( self.before = before self.user_id = user_id self.action_type = action_type - self.after = after or OLDEST_OBJECT + self.after = after self._users = {} self._state = guild._state self.entries = asyncio.Queue() async def _retrieve_entries(self, retrieve): + if not self._get_retrieve(): + return before = self.before.id if self.before else None after = self.after.id if self.after else None data: AuditLogPayload = await self.request( @@ -509,13 +511,14 @@ async def _retrieve_entries(self, retrieve): ) entries = data.get("audit_log_entries", []) + # if ONLY after is passed and NOT before, Discord reverses the sort to oldest first. if len(data) and entries: if self.limit is not None: self.limit -= retrieve if self.before or not self.after: self.before = Object(id=int(entries[-1]["id"])) - if self.after or not self.before: - self.after = Object(id=int(entries[0]["id"])) + if self.after and not self.before: + self.after = Object(id=int(entries[-1]["id"])) return data.get("users", []), entries async def next(self) -> AuditLogEntry: @@ -528,9 +531,13 @@ async def next(self) -> AuditLogEntry: raise NoMoreItems() def _get_retrieve(self): - limit = self.limit or 100 - self.retrieve = min(limit, 100) - return self.retrieve > 0 + l = self.limit + if l is None or l > 100: + r = 100 + else: + r = l + self.retrieve = r + return r > 0 async def _fill(self): from .user import User From 63da81389736bd94bc921035e607ed1ea628e7f5 Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:54:22 +0300 Subject: [PATCH 105/108] chore: improve nesting error message (#2275) * chore: improve subgroup limit error message * Changelog * style(pre-commit): auto fixes from pre-commit.com hooks * Update discord/commands/core.py Co-authored-by: Lala Sabathil Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> * Update core.py * style(pre-commit): auto fixes from pre-commit.com hooks * Update CHANGELOG.md * style(pre-commit): auto fixes from pre-commit.com hooks --------- Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil --- CHANGELOG.md | 2 ++ discord/commands/core.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c996d6e1e..320aa5b3fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -239,6 +239,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2170](https://github.com/Pycord-Development/pycord/pull/2170)) - `BridgeOption` must now be used for arguments in bridge commands. ([#2252](https://github.com/Pycord-Development/pycord/pull/2252)) +- Changed the error message that appears when attempting to add a subcommand group to a + subcommand group. ([#2275](https://github.com/Pycord-Development/pycord/pull/2275)) ### Removed diff --git a/discord/commands/core.py b/discord/commands/core.py index 3c886f2dfc..08d6f7780e 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -1425,8 +1425,7 @@ def create_subgroup( """ if self.parent is not None: - # TODO: Improve this error message - raise Exception("a subgroup cannot have a subgroup") + raise Exception("A subcommand group cannot be added to a subcommand group") sub_command_group = SlashCommandGroup( name, description, guild_ids, parent=self, **kwargs From 81dfa98e0abfc3cd00e98c6d08b49bdf412025aa Mon Sep 17 00:00:00 2001 From: Ice Wolfy <44532864+Icebluewolf@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:13:14 -0500 Subject: [PATCH 106/108] docs: add missing attributes to UserCommand and MessageCommand (#2494) --- discord/commands/core.py | 44 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/discord/commands/core.py b/discord/commands/core.py index 08d6f7780e..c1093351d9 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -1730,7 +1730,17 @@ class UserCommand(ContextMenuCommand): The coroutine that is executed when the command is called. guild_ids: Optional[List[:class:`int`]] The ids of the guilds where this command will be registered. - cog: Optional[:class:`.Cog`] + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + + .. deprecated:: 2.6 + Use the ``contexts`` parameter instead. + 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`] The cog that this command belongs to. ``None`` if there isn't one. checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] A list of predicates that verifies if the command could be executed @@ -1739,6 +1749,16 @@ class UserCommand(ContextMenuCommand): :exc:`.ApplicationCommandError` should be used. Note that if the checks fail then :exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error` event. + cooldown: Optional[:class:`~discord.ext.commands.Cooldown`] + The cooldown applied when the command is invoked. ``None`` if the command + doesn't have a cooldown. + name_localizations: Dict[:class:`str`, :class:`str`] + The name localizations for this command. The values of this should be ``"locale": "name"``. See + `here `_ for a list of valid locales. + integration_types: Set[:class:`IntegrationType`] + The installation contexts where this command is available. Unapplicable for guild commands. + contexts: Set[:class:`InteractionContextType`] + The interaction contexts where this command is available. Unapplicable for guild commands. """ type = 2 @@ -1829,7 +1849,17 @@ class MessageCommand(ContextMenuCommand): The coroutine that is executed when the command is called. guild_ids: Optional[List[:class:`int`]] The ids of the guilds where this command will be registered. - cog: Optional[:class:`.Cog`] + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + + .. deprecated:: 2.6 + Use the ``contexts`` parameter instead. + 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`] The cog that this command belongs to. ``None`` if there isn't one. checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] A list of predicates that verifies if the command could be executed @@ -1838,6 +1868,16 @@ class MessageCommand(ContextMenuCommand): :exc:`.ApplicationCommandError` should be used. Note that if the checks fail then :exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error` event. + cooldown: Optional[:class:`~discord.ext.commands.Cooldown`] + The cooldown applied when the command is invoked. ``None`` if the command + doesn't have a cooldown. + name_localizations: Dict[:class:`str`, :class:`str`] + The name localizations for this command. The values of this should be ``"locale": "name"``. See + `here `_ for a list of valid locales. + integration_types: Set[:class:`IntegrationType`] + The installation contexts where this command is available. Unapplicable for guild commands. + contexts: Set[:class:`InteractionContextType`] + The interaction contexts where this command is available. Unapplicable for guild commands. """ type = 3 From 77c9ff98930ccd6b796424b80d1fa4af73aa5dcd Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Tue, 9 Jul 2024 21:15:29 +0300 Subject: [PATCH 107/108] chore: update readme and changelog (#2493) * chore: fix incorrect pull ID * chore: update key features * chore: add dot * chore: add 2.6 header * chore: move the 2.6 header * chore: add complete changelog link * fix changelog position of #2275 * Update CHANGELOG.md --------- Co-authored-by: Lala Sabathil --- CHANGELOG.md | 13 ++++++++----- README.rst | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 320aa5b3fc..4712e2edbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ possible (see our [Version Guarantees] for more info). These changes are available on the `master` branch, but have not yet been released. +## [2.6.0] - 2024-07-09 + ### Added - Added `banner` parameter to `ClientUser.edit`. @@ -68,7 +70,7 @@ These changes are available on the `master` branch, but have not yet been releas ([#2448](https://github.com/Pycord-Development/pycord/pull/2448)) - Fixed missing `application_id` in `Entitlement.delete`. ([#2458](https://github.com/Pycord-Development/pycord/pull/2458)) -- Fixed issues with enums as `Option` types with long descriptions or too many values +- Fixed issues with enums as `Option` types with long descriptions or too many values. ([#2463](https://github.com/Pycord-Development/pycord/pull/2463)) - Fixed many inaccurate type hints throughout the library. ([#2457](https://github.com/Pycord-Development/pycord/pull/2457)) @@ -101,6 +103,8 @@ These changes are available on the `master` branch, but have not yet been releas - Replaced `Client.fetch_entitlements` with `Client.entitlements`, which returns an `EntitlementIterator`. ([#2490](https://github.com/Pycord-Development/pycord/pull/2490)) +- Changed the error message that appears when attempting to add a subcommand group to a + subcommand group. ([#2275](https://github.com/Pycord-Development/pycord/pull/2275)) ### Removed @@ -111,7 +115,7 @@ These changes are available on the `master` branch, but have not yet been releas and `after` parameters. ([#2371](https://github.com/Pycord-Development/pycord/pull/2371)) - Removed the `vanity_code` parameter from `Guild.edit`. - ([#2480](https://github.com/Pycord-Development/pycord/pull/2480)) + ([#2491](https://github.com/Pycord-Development/pycord/pull/2491)) ## [2.5.0] - 2024-03-02 @@ -239,8 +243,6 @@ These changes are available on the `master` branch, but have not yet been releas ([#2170](https://github.com/Pycord-Development/pycord/pull/2170)) - `BridgeOption` must now be used for arguments in bridge commands. ([#2252](https://github.com/Pycord-Development/pycord/pull/2252)) -- Changed the error message that appears when attempting to add a subcommand group to a - subcommand group. ([#2275](https://github.com/Pycord-Development/pycord/pull/2275)) ### Removed @@ -885,7 +887,8 @@ These changes are available on the `master` branch, but have not yet been releas - Fix py3.10 UnionType checks issue. ([#1240](https://github.com/Pycord-Development/pycord/pull/1240)) -[unreleased]: https://github.com/Pycord-Development/pycord/compare/v2.5.0...HEAD +[unreleased]: https://github.com/Pycord-Development/pycord/compare/v2.6.0...HEAD +[2.6.0]: https://github.com/Pycord-Development/pycord/compare/v2.5.0...2.6.0 [2.5.0]: https://github.com/Pycord-Development/pycord/compare/v2.4.1...v2.5.0 [2.4.1]: https://github.com/Pycord-Development/pycord/compare/v2.4.0...v2.4.1 [2.4.0]: https://github.com/Pycord-Development/pycord/compare/v2.3.3...v2.4.0 diff --git a/README.rst b/README.rst index a65654c4e6..ae74c857e0 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ Key Features - Modern Pythonic API using ``async`` and ``await``. - Proper rate limit handling. - Optimised for both speed and memory usage. -- Full Application Command Support +- Full application API support. Installing ---------- From ed1f73ccbf2b1cdbe08711ae1c4fc0a54d50523b Mon Sep 17 00:00:00 2001 From: plun1331 Date: Wed, 10 Jul 2024 08:07:02 -0700 Subject: [PATCH 108/108] docs: add docs for Interaction.entitlements (#2495) Signed-off-by: plun1331 --- discord/interactions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/discord/interactions.py b/discord/interactions.py index ee4df62014..e7d7fade3e 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -140,6 +140,10 @@ class Interaction: The guilds preferred locale, if invoked in a guild. custom_id: Optional[:class:`str`] The custom ID for the interaction. + entitlements: list[:class:`Entitlement`] + Entitlements that apply to the invoking user, showing access to premium SKUs. + + .. versionadded:: 2.5 authorizing_integration_owners: :class:`AuthorizingIntegrationOwners` Contains the entities (users or guilds) that authorized this interaction.