From 8d8d5e180adf8961cdac625e43f03bce99654f56 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:53:35 +0530 Subject: [PATCH 01/40] fix docs for soundboard_sound_update event --- docs/api.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e366f63bf312..dda5553b7074 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1354,8 +1354,10 @@ Soundboard .. versionadded:: 2.5 - :param sound: The soundboard sound that was updated. - :type sound: :class:`SoundboardSound` + :param before: The soundboard sound before the update. + :type before: :class:`SoundboardSound` + :param after: The soundboard sound after the update. + :type after: :class:`SoundboardSound` Stages From 7f16a06479bdae27b24916e784414613e8473f5d Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 20 Jun 2025 20:24:06 +0200 Subject: [PATCH 02/40] Copy Select options when creating View class --- discord/components.py | 3 +++ discord/ui/view.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/discord/components.py b/discord/components.py index b3f978eb1bc4..b62ab6bf9e08 100644 --- a/discord/components.py +++ b/discord/components.py @@ -442,6 +442,9 @@ def to_dict(self) -> SelectOptionPayload: return payload + def copy(self) -> SelectOption: + return self.__class__.from_dict(self.to_dict()) + class TextInput(Component): """Represents a text input from the Discord Bot UI Kit. diff --git a/discord/ui/view.py b/discord/ui/view.py index dd44944ec0ef..f27b71eeb90e 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -33,6 +33,7 @@ import time import os from .item import Item, ItemCallbackType +from .select import Select from .dynamic import DynamicItem from ..components import ( Component, @@ -179,6 +180,8 @@ def _init_children(self) -> List[Item[Self]]: item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) item.callback = _ViewCallback(func, self, item) # type: ignore item._view = self + if isinstance(item, Select): + item.options = [option.copy() for option in item.options] setattr(self, func.__name__, item) children.append(item) return children From 3e48119654411d1fbf4dafaae8f06c6a0d053cfd Mon Sep 17 00:00:00 2001 From: Maxine Date: Fri, 20 Jun 2025 20:24:55 +0200 Subject: [PATCH 03/40] Fix unreachable code not triggering privileged intent error --- discord/shard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/shard.py b/discord/shard.py index 454fd5e2895a..cd10cc265244 100644 --- a/discord/shard.py +++ b/discord/shard.py @@ -517,10 +517,10 @@ async def connect(self, *, reconnect: bool = True) -> None: if item.type == EventType.close: await self.close() if isinstance(item.error, ConnectionClosed): - if item.error.code != 1000: - raise item.error if item.error.code == 4014: raise PrivilegedIntentsRequired(item.shard.id) from None + if item.error.code != 1000: + raise item.error return elif item.type in (EventType.identify, EventType.resume): await item.shard.reidentify(item.error) From 0ce11544e3a32d0d4b9d867e3b1d356c4438e196 Mon Sep 17 00:00:00 2001 From: Dep <70801324+Depreca1ed@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:55:47 +0530 Subject: [PATCH 04/40] Add discord.Permissions.apps() --- discord/permissions.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/discord/permissions.py b/discord/permissions.py index b553e2578161..c234ad5f32b0 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -363,6 +363,16 @@ def elevated(cls) -> Self: """ return cls(0b0000_0000_0000_0000_0000_0001_0000_0100_0111_0000_0000_0000_0010_0000_0011_1110) + @classmethod + def apps(cls) -> Self: + """A factory method that creates a :class:`Permissions` with all + "Apps" permissions from the official Discord UI set to ``True``. + + + .. versionadded:: 2.6 + """ + return cls(0b0000_0000_0000_0100_0000_0000_1000_0000_1000_0000_0000_0000_0000_0000_0000_0000) + @classmethod def events(cls) -> Self: """A factory method that creates a :class:`Permissions` with all From aa5f4bdd7a8327fdcd1d68c96c1cc97c87cca534 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Fri, 20 Jun 2025 20:27:37 +0200 Subject: [PATCH 05/40] Fix poll parameter defaulting to MISSING instead of None for Context.send --- discord/ext/commands/context.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 7198c12064ec..b5b96c15f9a6 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -924,7 +924,7 @@ async def send( suppress_embeds: bool = False, ephemeral: bool = False, silent: bool = False, - poll: Poll = MISSING, + poll: Optional[Poll] = None, ) -> Message: """|coro| @@ -1014,10 +1014,12 @@ async def send( .. versionadded:: 2.2 - poll: :class:`~discord.Poll` + poll: Optional[:class:`~discord.Poll`] The poll to send with this message. .. versionadded:: 2.4 + .. versionchanged:: 2.6 + This can now be ``None`` and defaults to ``None`` instead of ``MISSING``. Raises -------- @@ -1072,7 +1074,7 @@ async def send( 'suppress_embeds': suppress_embeds, 'ephemeral': ephemeral, 'silent': silent, - 'poll': poll, + 'poll': MISSING if poll is None else poll, } if self.interaction.response.is_done(): From 680fe9bc808422cddc04968aa003ea6edf9090be Mon Sep 17 00:00:00 2001 From: Tari <65512380+Tari-dev@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:58:08 +0530 Subject: [PATCH 06/40] Add channel select to component type documentation --- docs/interactions/api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index feab669073ea..294a3b13a781 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -329,6 +329,10 @@ Enumerations Represents a select in which both users and roles can be selected. + .. attribute:: channel_select + + Represents a channel select component. + .. class:: ButtonStyle Represents the style of the button component. From e84edf473c1372c7c04b60abbbde4d660ce4256d Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 20 Jun 2025 20:28:59 +0200 Subject: [PATCH 07/40] Add colours for new Discord themes --- discord/colour.py | 83 +++++++++++++++++++++++++++++++++++++++----- tests/test_colour.py | 13 ++++--- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/discord/colour.py b/discord/colour.py index 7e3a37132a11..8c40dac35a1e 100644 --- a/discord/colour.py +++ b/discord/colour.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 import colorsys @@ -457,20 +458,59 @@ def greyple(cls) -> Self: """ return cls(0x99AAB5) + @classmethod + def ash_theme(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x2E2E34``. + + This will appear transparent on Discord's ash theme. + + .. colour:: #2E2E34 + + .. versionadded:: 2.6 + """ + return cls(0x2E2E34) + @classmethod def dark_theme(cls) -> Self: - """A factory method that returns a :class:`Colour` with a value of ``0x313338``. + """A factory method that returns a :class:`Colour` with a value of ``0x1A1A1E``. This will appear transparent on Discord's dark theme. - .. colour:: #313338 + .. colour:: #1A1A1E .. versionadded:: 1.5 .. versionchanged:: 2.2 Updated colour from previous ``0x36393F`` to reflect discord theme changes. + + .. versionchanged:: 2.6 + Updated colour from previous ``0x313338`` to reflect discord theme changes. """ - return cls(0x313338) + return cls(0x1A1A1E) + + @classmethod + def onyx_theme(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x070709``. + + This will appear transparent on Discord's onyx theme. + + .. colour:: #070709 + + .. versionadded:: 2.6 + """ + return cls(0x070709) + + @classmethod + def light_theme(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xFBFBFB``. + + This will appear transparent on Discord's light theme. + + .. colour:: #FBFBFB + + .. versionadded:: 2.6 + """ + return cls(0xFBFBFB) @classmethod def fuchsia(cls) -> Self: @@ -492,25 +532,52 @@ def yellow(cls) -> Self: """ return cls(0xFEE75C) + @classmethod + def ash_embed(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x37373E``. + + .. colour:: #37373E + + .. versionadded:: 2.6 + + """ + return cls(0x37373E) + @classmethod def dark_embed(cls) -> Self: - """A factory method that returns a :class:`Colour` with a value of ``0x2B2D31``. + """A factory method that returns a :class:`Colour` with a value of ``0x242429``. - .. colour:: #2B2D31 + .. colour:: #242429 .. versionadded:: 2.2 + + .. versionchanged:: 2.6 + Updated colour from previous ``0x2B2D31`` to reflect discord theme changes. + """ + return cls(0x242429) + + @classmethod + def onyx_embed(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x131416``. + + .. colour:: #131416 + + .. versionadded:: 2.6 """ - return cls(0x2B2D31) + return cls(0x131416) @classmethod def light_embed(cls) -> Self: - """A factory method that returns a :class:`Colour` with a value of ``0xEEEFF1``. + """A factory method that returns a :class:`Colour` with a value of ``0xFFFFFF``. .. colour:: #EEEFF1 .. versionadded:: 2.2 + + .. versionchanged:: 2.6 + Updated colour from previous ``0xEEEFF1`` to reflect discord theme changes. """ - return cls(0xEEEFF1) + return cls(0xFFFFFF) @classmethod def pink(cls) -> Self: diff --git a/tests/test_colour.py b/tests/test_colour.py index b79f153f06e8..1515b2cb47e4 100644 --- a/tests/test_colour.py +++ b/tests/test_colour.py @@ -106,11 +106,16 @@ def test_from_str_failures(value): (discord.Colour.og_blurple(), 0x7289DA), (discord.Colour.blurple(), 0x5865F2), (discord.Colour.greyple(), 0x99AAB5), - (discord.Colour.dark_theme(), 0x313338), + (discord.Colour.ash_theme(), 0x2E2E34), + (discord.Colour.dark_theme(), 0x1A1A1E), + (discord.Colour.onyx_theme(), 0x070709), + (discord.Colour.light_theme(), 0xFBFBFB), (discord.Colour.fuchsia(), 0xEB459E), (discord.Colour.yellow(), 0xFEE75C), - (discord.Colour.dark_embed(), 0x2B2D31), - (discord.Colour.light_embed(), 0xEEEFF1), + (discord.Colour.ash_embed(), 0x37373E), + (discord.Colour.dark_embed(), 0x242429), + (discord.Colour.onyx_embed(), 0x131416), + (discord.Colour.light_embed(), 0xFFFFFF), (discord.Colour.pink(), 0xEB459F), ], ) @@ -118,8 +123,6 @@ def test_static_colours(value, expected): assert value.value == expected - - @pytest.mark.parametrize( ('value', 'property', 'expected'), [ From e177b4a70502cf248ae0d682ed011875999ffd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Fri, 20 Jun 2025 19:30:10 +0100 Subject: [PATCH 08/40] Fix EmbedMediaProxy boolean check --- discord/embeds.py | 6 ++++++ tests/test_embed.py | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/discord/embeds.py b/discord/embeds.py index 7f84e410d341..6bd057ac86a8 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -61,6 +61,12 @@ def __init__(self, layer: Dict[str, Any]): super().__init__(layer) self._flags = self.__dict__.pop('flags', 0) + def __bool__(self) -> bool: + # This is a nasty check to see if we only have the `_flags` attribute which is created regardless in init. + # Had we had any of the other items, like image/video data this would be >1 and therefor + # would not be "empty". + return len(self.__dict__) > 1 + @property def flags(self) -> AttachmentFlags: return AttachmentFlags._from_value(self._flags or 0) diff --git a/tests/test_embed.py b/tests/test_embed.py index 3efedd6a57be..004f73e3bc91 100644 --- a/tests/test_embed.py +++ b/tests/test_embed.py @@ -267,3 +267,14 @@ def test_embed_colour_setter_failure(value): embed = discord.Embed() with pytest.raises(TypeError): embed.colour = value + +@pytest.mark.parametrize( + ('title', 'return_val'), + [ + ('test', True), + (None, False) + ] +) +def test_embed_truthiness(title: str, return_val: bool) -> None: + embed = discord.Embed(title=title) + assert bool(embed) is return_val From ef06d7d9db2b868c57f45fe1252d055490ad31a9 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 20 Jun 2025 20:30:37 +0200 Subject: [PATCH 09/40] Add Interaction.filesize_limit --- discord/interactions.py | 8 +++++++- discord/types/interactions.py | 1 + tests/test_app_commands_invoke.py | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/discord/interactions.py b/discord/interactions.py index a983d8ab04f6..cb9a21e88753 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -154,6 +154,10 @@ class Interaction(Generic[ClientT]): The context of the interaction. .. versionadded:: 2.4 + filesize_limit: int + The maximum number of bytes a file can have when responding to this interaction. + + .. versionadded:: 2.6 """ __slots__: Tuple[str, ...] = ( @@ -172,7 +176,8 @@ class Interaction(Generic[ClientT]): 'command_failed', 'entitlement_sku_ids', 'entitlements', - "context", + 'context', + 'filesize_limit', '_integration_owners', '_permissions', '_app_permissions', @@ -214,6 +219,7 @@ def _from_data(self, data: InteractionPayload): self.application_id: int = int(data['application_id']) self.entitlement_sku_ids: List[int] = [int(x) for x in data.get('entitlement_skus', []) or []] self.entitlements: List[Entitlement] = [Entitlement(self._state, x) for x in data.get('entitlements', [])] + self.filesize_limit: int = data['attachment_size_limit'] # This is not entirely useful currently, unsure how to expose it in a way that it is. self._integration_owners: Dict[int, Snowflake] = { int(k): int(v) for k, v in data.get('authorizing_integration_owners', {}).items() diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 3f3516c3a696..3e814b49df6e 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -233,6 +233,7 @@ class _BaseInteraction(TypedDict): entitlements: NotRequired[List[Entitlement]] authorizing_integration_owners: Dict[Literal['0', '1'], Snowflake] context: NotRequired[InteractionContextType] + attachment_size_limit: int class PingInteraction(_BaseInteraction): diff --git a/tests/test_app_commands_invoke.py b/tests/test_app_commands_invoke.py index 35915c19b9ee..6366096f01a1 100644 --- a/tests/test_app_commands_invoke.py +++ b/tests/test_app_commands_invoke.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 @@ -90,6 +91,7 @@ def __init__( "version": 1, "type": 2, "data": self._get_command_data(command, self._get_command_options(**options)), + "attachment_size_limit": 0, } super().__init__(data=data, state=client._connection) From b28a4a115e72977a9091091983a1213e91f8a5e9 Mon Sep 17 00:00:00 2001 From: Michael H Date: Fri, 20 Jun 2025 14:31:55 -0400 Subject: [PATCH 10/40] Fix potentially stuck ratelimit buckets --- discord/http.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index 6617efa2708b..eb1dbbd06832 100644 --- a/discord/http.py +++ b/discord/http.py @@ -461,7 +461,12 @@ async def acquire(self) -> None: future = self._loop.create_future() self._pending_requests.append(future) try: - await future + while not future.done(): + # 30 matches the smallest allowed max_ratelimit_timeout + max_wait_time = self.expires - self._loop.time() if self.expires else 30 + await asyncio.wait([future], timeout=max_wait_time) + if not future.done(): + await self._refresh() except: future.cancel() if self.remaining > 0 and not future.cancelled(): From adb93f4a606ae21a87b46b3f150a75ed6c7d86bb Mon Sep 17 00:00:00 2001 From: Gooraeng <101193491+Gooraeng@users.noreply.github.com> Date: Sat, 21 Jun 2025 03:36:24 +0900 Subject: [PATCH 11/40] Add deprecation warning to create_guild functions --- discord/client.py | 6 +++++- discord/template.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/discord/client.py b/discord/client.py index b997bd96f4af..daf2b8855446 100644 --- a/discord/client.py +++ b/discord/client.py @@ -67,7 +67,7 @@ from .http import HTTPClient from .state import ConnectionState from . import utils -from .utils import MISSING, time_snowflake +from .utils import MISSING, time_snowflake, deprecated from .object import Object from .backoff import ExponentialBackoff from .webhook import Webhook @@ -2388,6 +2388,7 @@ async def fetch_guild_preview(self, guild_id: int) -> GuildPreview: data = await self.http.get_guild_preview(guild_id) return GuildPreview(data=data, state=self._connection) + @deprecated() async def create_guild( self, *, @@ -2408,6 +2409,9 @@ async def create_guild( This function will now raise :exc:`ValueError` instead of ``InvalidArgument``. + .. deprecated:: 2.6 + This function is deprecated and will be removed in a future version. + Parameters ---------- name: :class:`str` diff --git a/discord/template.py b/discord/template.py index 409cdc7d9c55..691be2cafa67 100644 --- a/discord/template.py +++ b/discord/template.py @@ -25,7 +25,7 @@ from __future__ import annotations from typing import Any, Optional, TYPE_CHECKING, List -from .utils import parse_time, _bytes_to_base64_data, MISSING +from .utils import parse_time, _bytes_to_base64_data, MISSING, deprecated from .guild import Guild # fmt: off @@ -164,6 +164,7 @@ def __repr__(self) -> str: f' creator={self.creator!r} source_guild={self.source_guild!r} is_dirty={self.is_dirty}>' ) + @deprecated() async def create_guild(self, name: str, icon: bytes = MISSING) -> Guild: """|coro| @@ -178,6 +179,9 @@ async def create_guild(self, name: str, icon: bytes = MISSING) -> Guild: This function will now raise :exc:`ValueError` instead of ``InvalidArgument``. + .. deprecated:: 2.6 + This function is deprecated and will be removed in a future version. + Parameters ---------- name: :class:`str` From 4a8817af2b89c9833ef4d889aabd6a57acad7a30 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Fri, 20 Jun 2025 20:49:20 +0200 Subject: [PATCH 12/40] Document return object for Guild.create_template --- discord/guild.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index 20a50d4e932f..291363b1885c 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2921,6 +2921,11 @@ async def create_template(self, *, name: str, description: str = MISSING) -> Tem The name of the template. description: :class:`str` The description of the template. + + Returns + -------- + :class:`Template` + The created template. """ from .template import Template From 667e7c906523a20fea8313b3eb59bdd8019525ff Mon Sep 17 00:00:00 2001 From: Roberto Scifo Date: Fri, 20 Jun 2025 20:55:44 +0200 Subject: [PATCH 13/40] Fixed to_dict() bug for user-defined embed classes --- discord/embeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/embeds.py b/discord/embeds.py index 6bd057ac86a8..f55c7cac1346 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -743,7 +743,7 @@ def to_dict(self) -> EmbedData: # fmt: off result = { key[1:]: getattr(self, key) - for key in self.__slots__ + for key in Embed.__slots__ if key[0] == '_' and hasattr(self, key) } # fmt: on From 2bcbd49bc6d618736e2aa2c5c4d6beb76e33238c Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:27:10 +0530 Subject: [PATCH 14/40] Add __repr__ method to various classes --- discord/app_commands/installs.py | 6 ++++++ discord/app_commands/models.py | 6 ++++++ discord/ext/commands/parameters.py | 3 +++ discord/interactions.py | 3 +++ 4 files changed, 18 insertions(+) diff --git a/discord/app_commands/installs.py b/discord/app_commands/installs.py index 5ac033245ab7..e00d13724031 100644 --- a/discord/app_commands/installs.py +++ b/discord/app_commands/installs.py @@ -57,6 +57,9 @@ def __init__(self, *, guild: Optional[bool] = None, user: Optional[bool] = None) self._guild: Optional[bool] = guild self._user: Optional[bool] = user + def __repr__(self): + return f'' + @property def guild(self) -> bool: """:class:`bool`: Whether the integration is a guild install.""" @@ -142,6 +145,9 @@ def __init__( self._dm_channel: Optional[bool] = dm_channel self._private_channel: Optional[bool] = private_channel + def __repr__(self) -> str: + return f'' + @property def guild(self) -> bool: """:class:`bool`: Whether the context allows usage in a guild.""" diff --git a/discord/app_commands/models.py b/discord/app_commands/models.py index e8a96784b87c..dcf70d41e0a4 100644 --- a/discord/app_commands/models.py +++ b/discord/app_commands/models.py @@ -1063,6 +1063,9 @@ def __init__(self, *, data: ApplicationCommandPermissions, guild: Guild, state: self.target: Union[Object, User, Member, Role, AllChannels, GuildChannel] = _object + def __repr__(self) -> str: + return f'' + def to_dict(self) -> ApplicationCommandPermissions: return { 'id': self.target.id, @@ -1106,6 +1109,9 @@ def __init__(self, *, data: GuildApplicationCommandPermissions, state: Connectio AppCommandPermissions(data=value, guild=guild, state=self._state) for value in data['permissions'] ] + def __repr__(self) -> str: + return f'' + def to_dict(self) -> Dict[str, Any]: return {'permissions': [p.to_dict() for p in self.permissions]} diff --git a/discord/ext/commands/parameters.py b/discord/ext/commands/parameters.py index 196530d94c8b..2640902a34b5 100644 --- a/discord/ext/commands/parameters.py +++ b/discord/ext/commands/parameters.py @@ -109,6 +109,9 @@ def __init__( self._fallback = False self._displayed_name = displayed_name + def __repr__(self) -> str: + return f'<{self.__class__.__name__} name={self._name!r} required={self.required}>' + def replace( self, *, diff --git a/discord/interactions.py b/discord/interactions.py index cb9a21e88753..abe47efa2b7d 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -712,6 +712,9 @@ def __init__( self.type: InteractionResponseType = type self._update(data) + def __repr__(self) -> str: + return f'' + def _update(self, data: InteractionCallbackPayload) -> None: interaction = data['interaction'] From c524f655beecd5ed4c11f900fe7077fe17248e4f Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:27:52 +0530 Subject: [PATCH 15/40] Use human_join internal helper in BaseChannelTransformer --- discord/app_commands/transformers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/app_commands/transformers.py b/discord/app_commands/transformers.py index c18485d8c0b4..58253a497290 100644 --- a/discord/app_commands/transformers.py +++ b/discord/app_commands/transformers.py @@ -52,7 +52,7 @@ from ..abc import GuildChannel from ..threads import Thread from ..enums import Enum as InternalEnum, AppCommandOptionType, ChannelType, Locale -from ..utils import MISSING, maybe_coroutine +from ..utils import MISSING, maybe_coroutine, _human_join from ..user import User from ..role import Role from ..member import Member @@ -631,7 +631,7 @@ def __init__(self, *channel_types: Type[Any]) -> None: display_name = channel_types[0].__name__ types = CHANNEL_TO_TYPES[channel_types[0]] else: - display_name = '{}, and {}'.format(', '.join(t.__name__ for t in channel_types[:-1]), channel_types[-1].__name__) + display_name = _human_join([t.__name__ for t in channel_types]) types = [] for t in channel_types: From 826aa22868b1552a6a6f28d567b8f62c694aab40 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:28:30 +0530 Subject: [PATCH 16/40] Add platform key in __repr__ method for Game and Streaming --- discord/activity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/activity.py b/discord/activity.py index 324bea42f290..0fc0faa64652 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -418,7 +418,7 @@ def __str__(self) -> str: return str(self.name) def __repr__(self) -> str: - return f'' + return f'' def to_dict(self) -> Dict[str, Any]: timestamps: Dict[str, Any] = {} @@ -514,7 +514,7 @@ def __str__(self) -> str: return str(self.name) def __repr__(self) -> str: - return f'' + return f'' @property def twitch_name(self) -> Optional[str]: From fe942d1e16d3c6fa1b350fa971d80b72ab0bba1c Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:29:07 +0530 Subject: [PATCH 17/40] Properly transform media channels in app commands --- discord/app_commands/transformers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/app_commands/transformers.py b/discord/app_commands/transformers.py index 58253a497290..212991cbe372 100644 --- a/discord/app_commands/transformers.py +++ b/discord/app_commands/transformers.py @@ -689,6 +689,7 @@ async def transform(self, interaction: Interaction[ClientT], value: Any, /): ChannelType.news, ChannelType.category, ChannelType.forum, + ChannelType.media, ], GuildChannel: [ ChannelType.stage_voice, @@ -697,6 +698,7 @@ async def transform(self, interaction: Interaction[ClientT], value: Any, /): ChannelType.news, ChannelType.category, ChannelType.forum, + ChannelType.media, ], AppCommandThread: [ChannelType.news_thread, ChannelType.private_thread, ChannelType.public_thread], Thread: [ChannelType.news_thread, ChannelType.private_thread, ChannelType.public_thread], @@ -704,7 +706,7 @@ async def transform(self, interaction: Interaction[ClientT], value: Any, /): VoiceChannel: [ChannelType.voice], TextChannel: [ChannelType.text, ChannelType.news], CategoryChannel: [ChannelType.category], - ForumChannel: [ChannelType.forum], + ForumChannel: [ChannelType.forum, ChannelType.media], } BUILT_IN_TRANSFORMERS: Dict[Any, Transformer] = { From d00c124883c50910652b9d6cf8e6b1908930d9e3 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:31:48 +0530 Subject: [PATCH 18/40] Add MessageApplication.__str__ --- discord/message.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/message.py b/discord/message.py index 547e9c433aed..d6a26c7d0d6f 100644 --- a/discord/message.py +++ b/discord/message.py @@ -989,6 +989,9 @@ def __init__(self, *, state: ConnectionState, data: MessageApplicationPayload) - self._icon: Optional[str] = data['icon'] self._cover_image: Optional[str] = data.get('cover_image') + def __str__(self) -> str: + return self.name + def __repr__(self) -> str: return f'' From 4862ea22fdac71359627b475e194fadaacb12612 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:33:29 +0530 Subject: [PATCH 19/40] Return invites when deleting them via Invite.delete or Client.delete_invite --- discord/client.py | 5 +++-- discord/http.py | 2 +- discord/invite.py | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/discord/client.py b/discord/client.py index daf2b8855446..68422435bfa8 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2550,7 +2550,7 @@ async def fetch_invite( ) return Invite.from_incomplete(state=self._connection, data=data) - async def delete_invite(self, invite: Union[Invite, str], /) -> None: + async def delete_invite(self, invite: Union[Invite, str], /) -> Invite: """|coro| Revokes an :class:`.Invite`, URL, or ID to an invite. @@ -2578,7 +2578,8 @@ async def delete_invite(self, invite: Union[Invite, str], /) -> None: """ resolved = utils.resolve_invite(invite) - await self.http.delete_invite(resolved.code) + data = await self.http.delete_invite(resolved.code) + return Invite.from_incomplete(state=self._connection, data=data) # Miscellaneous stuff diff --git a/discord/http.py b/discord/http.py index eb1dbbd06832..7d59c8bfb8b6 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1879,7 +1879,7 @@ def invites_from(self, guild_id: Snowflake) -> Response[List[invite.Invite]]: def invites_from_channel(self, channel_id: Snowflake) -> Response[List[invite.Invite]]: return self.request(Route('GET', '/channels/{channel_id}/invites', channel_id=channel_id)) - def delete_invite(self, invite_id: str, *, reason: Optional[str] = None) -> Response[None]: + def delete_invite(self, invite_id: str, *, reason: Optional[str] = None) -> Response[invite.Invite]: return self.request(Route('DELETE', '/invites/{invite_id}', invite_id=invite_id), reason=reason) # Role management diff --git a/discord/invite.py b/discord/invite.py index dd8cc954ac53..8c37bd232ca4 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -546,7 +546,7 @@ def set_scheduled_event(self, scheduled_event: Snowflake, /) -> Self: return self - async def delete(self, *, reason: Optional[str] = None) -> None: + async def delete(self, *, reason: Optional[str] = None) -> Self: """|coro| Revokes the instant invite. @@ -568,4 +568,5 @@ async def delete(self, *, reason: Optional[str] = None) -> None: Revoking the invite failed. """ - await self._state.http.delete_invite(self.code, reason=reason) + data = await self._state.http.delete_invite(self.code, reason=reason) + return self.from_incomplete(state=self._state, data=data) From f6e0f72498794591e2b87b8f85682823556f0c73 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:04:21 +0200 Subject: [PATCH 20/40] Add ability to create a media-only forum channel --- discord/guild.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 291363b1885c..2e834f9b2824 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1800,6 +1800,7 @@ async def create_forum( category: Optional[CategoryChannel] = None, slowmode_delay: int = MISSING, nsfw: bool = MISSING, + media: bool = MISSING, overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, reason: Optional[str] = None, default_auto_archive_duration: int = MISSING, @@ -1862,12 +1863,17 @@ async def create_forum( .. versionadded:: 2.3 default_layout: :class:`ForumLayoutType` The default layout for posts in this forum. + This cannot be set if ``media`` is set to ``True``. .. versionadded:: 2.3 available_tags: Sequence[:class:`ForumTag`] The available tags for this forum channel. .. versionadded:: 2.1 + media: :class:`bool` + Whether to create a media forum channel. + + .. versionadded:: 2.6 Raises ------- @@ -1919,7 +1925,7 @@ async def create_forum( else: raise ValueError(f'default_reaction_emoji parameter must be either Emoji, PartialEmoji, or str') - if default_layout is not MISSING: + if not media and default_layout is not MISSING: if not isinstance(default_layout, ForumLayoutType): raise TypeError( f'default_layout parameter must be a ForumLayoutType not {default_layout.__class__.__name__}' @@ -1931,10 +1937,17 @@ async def create_forum( options['available_tags'] = [t.to_dict() for t in available_tags] data = await self._create_channel( - name=name, overwrites=overwrites, channel_type=ChannelType.forum, category=category, reason=reason, **options + name=name, + overwrites=overwrites, + channel_type=ChannelType.forum if not media else ChannelType.media, + category=category, + reason=reason, + **options, ) - channel = ForumChannel(state=self._state, guild=self, data=data) + channel = ForumChannel( + state=self._state, guild=self, data=data # pyright: ignore[reportArgumentType] # it's the correct data + ) # temporarily add to the cache self._channels[channel.id] = channel From 6af9de0c39bdc51e02c05d0c5bd93a77809939b3 Mon Sep 17 00:00:00 2001 From: Michael H Date: Fri, 20 Jun 2025 15:06:52 -0400 Subject: [PATCH 21/40] Ensure COPYING notice is included --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index e623df089b9d..8e93fd092a5d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include README.rst include LICENSE include requirements.txt -include discord/bin/*.dll +include discord/bin/* include discord/py.typed From 7fe1102841987698d9ca445b05badf950bdf6114 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:07:23 +0200 Subject: [PATCH 22/40] Allow creating NSFW voice/stage channels --- discord/guild.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index 2e834f9b2824..6b8e8814e4c3 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1560,6 +1560,7 @@ async def create_voice_channel( rtc_region: Optional[str] = MISSING, video_quality_mode: VideoQualityMode = MISSING, overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, + nsfw: bool = MISSING, ) -> VoiceChannel: """|coro| @@ -1597,6 +1598,10 @@ async def create_voice_channel( The camera video quality for the voice channel's participants. .. versionadded:: 2.0 + nsfw: :class:`bool` + To mark the channel as NSFW or not. + + .. versionadded:: 2.6 reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. @@ -1632,6 +1637,9 @@ async def create_voice_channel( raise TypeError('video_quality_mode must be of type VideoQualityMode') options['video_quality_mode'] = video_quality_mode.value + if nsfw is not MISSING: + options['nsfw'] = nsfw + data = await self._create_channel( name, overwrites=overwrites, channel_type=ChannelType.voice, category=category, reason=reason, **options ) @@ -1653,6 +1661,7 @@ async def create_stage_channel( rtc_region: Optional[str] = MISSING, video_quality_mode: VideoQualityMode = MISSING, overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, + nsfw: bool = MISSING, ) -> StageChannel: """|coro| @@ -1696,6 +1705,10 @@ async def create_stage_channel( The camera video quality for the voice channel's participants. .. versionadded:: 2.2 + nsfw: :class:`bool` + To mark the channel as NSFW or not. + + .. versionadded:: 2.6 reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. @@ -1732,6 +1745,9 @@ async def create_stage_channel( raise TypeError('video_quality_mode must be of type VideoQualityMode') options['video_quality_mode'] = video_quality_mode.value + if nsfw is not MISSING: + options['nsfw'] = nsfw + data = await self._create_channel( name, overwrites=overwrites, channel_type=ChannelType.stage_voice, category=category, reason=reason, **options ) From e9f807e5ecee491e78f5e36092f0d818f2c7dd33 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:50:20 +0530 Subject: [PATCH 23/40] Fix Thread.applied_tags for media channels --- discord/threads.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/threads.py b/discord/threads.py index 024b22506b04..0c8060193f87 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -272,12 +272,12 @@ def applied_tags(self) -> List[ForumTag]: .. versionadded:: 2.1 """ tags = [] - if self.parent is None or self.parent.type != ChannelType.forum: + if self.parent is None or self.parent.type not in (ChannelType.forum, ChannelType.media): return tags parent = self.parent for tag_id in self._applied_tags: - tag = parent.get_tag(tag_id) + tag = parent.get_tag(tag_id) # type: ignore # parent here will be ForumChannel instance if tag is not None: tags.append(tag) From 44fb360a835bd5fdcdb1d69affe3ca2f570bc49d Mon Sep 17 00:00:00 2001 From: makerze <99765898+makerze@users.noreply.github.com> Date: Fri, 20 Jun 2025 20:25:54 +0100 Subject: [PATCH 24/40] Add new role parameters for creation of gradient and holographic roles --- discord/guild.py | 50 ++++++++++++++++++++++++++++++++++++++++++ discord/http.py | 2 +- discord/role.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 20a50d4e932f..8badf1239076 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3614,6 +3614,9 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., + primary_color: Union[Colour, int, None] = ..., + secondary_color: Union[Colour, int, None] = ..., + tertiary_color: Union[Colour, int, None] = ..., ) -> Role: ... @@ -3628,6 +3631,9 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., + primary_color: Union[Colour, int, None] = ..., + secondary_color: Union[Colour, int, None] = ..., + tertiary_color: Union[Colour, int, None] = ..., ) -> Role: ... @@ -3642,6 +3648,9 @@ async def create_role( display_icon: Union[bytes, str] = MISSING, mentionable: bool = MISSING, reason: Optional[str] = None, + primary_color: Union[Colour, int, None] = MISSING, + secondary_color: Union[Colour, int, None] = MISSING, + tertiary_color: Union[Colour, int, None] = MISSING, ) -> Role: """|coro| @@ -3670,6 +3679,14 @@ async def create_role( colour: Union[:class:`Colour`, :class:`int`] The colour for the role. Defaults to :meth:`Colour.default`. This is aliased to ``color`` as well. + primary_color: Union[:class:`Colour`, :class:`int`, None] + The primary color for the role. If provided, must be an integer or :class:`Colour`. + secondary_color: Union[:class:`Colour`, :class:`int`, None] + The secondary color for the role. Requires ``primary_color`` to also be set. + tertiary_color: Union[:class:`Colour`, :class:`int`, None] + The tertiary_color color for the role. Used for holographic role. + The holographic preset is: + {"primary_color": 11127295, "secondary_color": 16759788, "tertiary_color": 16761760} hoist: :class:`bool` Indicates if the role should be shown separately in the member list. Defaults to ``False``. @@ -3710,6 +3727,39 @@ async def create_role( else: fields['color'] = actual_colour.value + colors_payload: Dict[str, Any]= {} + if primary_color is not MISSING: + if primary_color is None: + colors_payload['primary_color'] = None + elif isinstance(primary_color, int): + colors_payload['primary_color'] = primary_color + else: + colors_payload['primary_color'] = primary_color.value + if secondary_color is not MISSING: + if secondary_color is None: + colors_payload['secondary_color'] = None + elif isinstance(secondary_color, int): + colors_payload['secondary_color'] = secondary_color + else: + colors_payload['secondary_color'] = secondary_color.value + if tertiary_color is not MISSING: + if tertiary_color is None: + colors_payload['tertiary_color'] = None + elif isinstance(tertiary_color, int): + colors_payload['tertiary_color'] = tertiary_color + else: + colors_payload['tertiary_color'] = tertiary_color.value + + if colors_payload: + fields['colors'] = colors_payload + + if not colors_payload: + actual_colour = colour or color or Colour.default() + if isinstance(actual_colour, int): + fields['color'] = actual_colour + else: + fields['color'] = actual_colour.value + if hoist is not MISSING: fields['hoist'] = hoist diff --git a/discord/http.py b/discord/http.py index 6617efa2708b..187126ed990d 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1889,7 +1889,7 @@ def edit_role( self, guild_id: Snowflake, role_id: Snowflake, *, reason: Optional[str] = None, **fields: Any ) -> Response[role.Role]: r = Route('PATCH', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id) - valid_keys = ('name', 'permissions', 'color', 'hoist', 'icon', 'unicode_emoji', 'mentionable') + valid_keys = ('name', 'permissions', 'color', 'hoist', 'icon', 'unicode_emoji', 'mentionable', 'colors') payload = {k: v for k, v in fields.items() if k in valid_keys} return self.request(r, json=payload, reason=reason) diff --git a/discord/role.py b/discord/role.py index d7fe1e08bbe2..076b0b9f748e 100644 --- a/discord/role.py +++ b/discord/role.py @@ -222,6 +222,9 @@ class Role(Hashable): 'tags', '_flags', '_state', + '_primary_color', + '_secondary_color', + '_tertiary_color', ) def __init__(self, *, guild: Guild, state: ConnectionState, data: RolePayload): @@ -284,6 +287,10 @@ def _update(self, data: RolePayload): self.mentionable: bool = data.get('mentionable', False) self.tags: Optional[RoleTags] self._flags: int = data.get('flags', 0) + colors = data.get('colors', {}) + self._primary_color = colors.get('primary_color', None) + self._secondary_color = colors.get('secondary_color', None) + self._tertiary_color = colors.get('tertiary_color', None) try: self.tags = RoleTags(data['tags']) # pyright: ignore[reportTypedDictNotRequiredAccess] @@ -324,6 +331,20 @@ def is_assignable(self) -> bool: return not self.is_default() and not self.managed and (me.top_role > self or me.id == self.guild.owner_id) @property + def primary_color(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: The role's primary color.""" + return Colour(self._primary_color) if self._primary_color is not None else None + + @property + def secondary_color(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: The role's secondary color.""" + return Colour(self._secondary_color) if self._secondary_color is not None else None + + @property + def tertiary_color(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: The role's tertiary color.""" + return Colour(self._tertiary_color) if self._tertiary_color is not None else None + @property def permissions(self) -> Permissions: """:class:`Permissions`: Returns the role's permissions.""" return Permissions(self._permissions) @@ -425,6 +446,9 @@ async def edit( mentionable: bool = MISSING, position: int = MISSING, reason: Optional[str] = MISSING, + primary_color: Union[Colour, int, None] = MISSING, + secondary_color: Union[Colour, int, None] = MISSING, + tertiary_color: Union[Colour, int, None] = MISSING, ) -> Optional[Role]: """|coro| @@ -470,6 +494,14 @@ async def edit( position or it will fail. reason: Optional[:class:`str`] The reason for editing this role. Shows up on the audit log. + primary_color: Union[:class:`Colour`, :class:`int`, None] + The primary color for the role. If provided, must be an integer or :class:`Colour`. + secondary_color: Union[:class:`Colour`, :class:`int`, None] + The secondary color for the role. + tertiary_color: Union[:class:`Colour`, :class:`int`, None] + The tertiary_color color for the role. Used for holographic role. + The holographic preset is: + {"primary_color": 11127295, "secondary_color": 16759788, "tertiary_color": 16761760} Raises ------- @@ -519,6 +551,31 @@ async def edit( if mentionable is not MISSING: payload['mentionable'] = mentionable + colors_payload: Dict[str, Any] = {} + if primary_color is not MISSING: + if primary_color is None: + colors_payload['primary_color'] = None + elif isinstance(primary_color, int): + colors_payload['primary_color'] = primary_color + else: + colors_payload['primary_color'] = primary_color.value + if secondary_color is not MISSING: + if secondary_color is None: + colors_payload['secondary_color'] = None + elif isinstance(secondary_color, int): + colors_payload['secondary_color'] = secondary_color + else: + colors_payload['secondary_color'] = secondary_color.value + if tertiary_color is not MISSING: + if tertiary_color is None: + colors_payload['tertiary_color'] = None + elif isinstance(tertiary_color, int): + colors_payload['tertiary_color'] = tertiary_color + else: + colors_payload['tertiary_color'] = tertiary_color.value + if colors_payload: + payload['colors'] = colors_payload + data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) return Role(guild=self.guild, data=data, state=self._state) From 4d058136fc88b567b9dd31f95835ca4bb3b9b414 Mon Sep 17 00:00:00 2001 From: makerze <99765898+makerze@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:17:16 +0100 Subject: [PATCH 25/40] Fix: throw exception when color is used together with colors parameters --- discord/guild.py | 11 +++++++++++ discord/role.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index 8badf1239076..fda921f9459a 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3727,6 +3727,17 @@ async def create_role( else: fields['color'] = actual_colour.value + solid_color_used = color is not MISSING or colour is not MISSING + colors_used = ( + primary_color is not MISSING or + secondary_color is not MISSING or + tertiary_color is not MISSING + ) + if solid_color_used and colors_used: + raise TypeError( + "You must choose either only solid color (color/colour) or colors (primary_color/secondary_color/tertiary_color), not both." + ) + colors_payload: Dict[str, Any]= {} if primary_color is not MISSING: if primary_color is None: diff --git a/discord/role.py b/discord/role.py index 076b0b9f748e..929d4446b79c 100644 --- a/discord/role.py +++ b/discord/role.py @@ -551,6 +551,17 @@ async def edit( if mentionable is not MISSING: payload['mentionable'] = mentionable + solid_color_used = color is not MISSING or colour is not MISSING + colors_used = ( + primary_color is not MISSING or + secondary_color is not MISSING or + tertiary_color is not MISSING + ) + if solid_color_used and colors_used: + raise TypeError( + "You must choose either only solid color (color/colour) or colors (primary_color/secondary_color/tertiary_color), not both." + ) + colors_payload: Dict[str, Any] = {} if primary_color is not MISSING: if primary_color is None: From 89961e52fa31f35e6d05389faad916fbc41cf018 Mon Sep 17 00:00:00 2001 From: makerze <99765898+makerze@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:27:15 +0100 Subject: [PATCH 26/40] linter --- discord/guild.py | 54 ++++++++++++++++-------------------------------- discord/role.py | 25 ++++++++-------------- 2 files changed, 27 insertions(+), 52 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index fda921f9459a..278b78cd3213 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1293,8 +1293,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, TextChannelPayload]: - ... + ) -> Coroutine[Any, Any, TextChannelPayload]: ... @overload def _create_channel( @@ -1304,8 +1303,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, VoiceChannelPayload]: - ... + ) -> Coroutine[Any, Any, VoiceChannelPayload]: ... @overload def _create_channel( @@ -1315,8 +1313,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, StageChannelPayload]: - ... + ) -> Coroutine[Any, Any, StageChannelPayload]: ... @overload def _create_channel( @@ -1326,8 +1323,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, CategoryChannelPayload]: - ... + ) -> Coroutine[Any, Any, CategoryChannelPayload]: ... @overload def _create_channel( @@ -1337,8 +1333,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, NewsChannelPayload]: - ... + ) -> Coroutine[Any, Any, NewsChannelPayload]: ... @overload def _create_channel( @@ -1348,8 +1343,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: - ... + ) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: ... @overload def _create_channel( @@ -1359,8 +1353,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, ForumChannelPayload]: - ... + ) -> Coroutine[Any, Any, ForumChannelPayload]: ... @overload def _create_channel( @@ -1370,8 +1363,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, GuildChannelPayload]: - ... + ) -> Coroutine[Any, Any, GuildChannelPayload]: ... def _create_channel( self, @@ -3202,8 +3194,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... @overload async def create_scheduled_event( @@ -3218,8 +3209,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... @overload async def create_scheduled_event( @@ -3233,8 +3223,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... @overload async def create_scheduled_event( @@ -3248,8 +3237,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... async def create_scheduled_event( self, @@ -3617,8 +3605,7 @@ async def create_role( primary_color: Union[Colour, int, None] = ..., secondary_color: Union[Colour, int, None] = ..., tertiary_color: Union[Colour, int, None] = ..., - ) -> Role: - ... + ) -> Role: ... @overload async def create_role( @@ -3634,8 +3621,7 @@ async def create_role( primary_color: Union[Colour, int, None] = ..., secondary_color: Union[Colour, int, None] = ..., tertiary_color: Union[Colour, int, None] = ..., - ) -> Role: - ... + ) -> Role: ... async def create_role( self, @@ -3728,17 +3714,13 @@ async def create_role( fields['color'] = actual_colour.value solid_color_used = color is not MISSING or colour is not MISSING - colors_used = ( - primary_color is not MISSING or - secondary_color is not MISSING or - tertiary_color is not MISSING - ) + colors_used = primary_color is not MISSING or secondary_color is not MISSING or tertiary_color is not MISSING if solid_color_used and colors_used: raise TypeError( "You must choose either only solid color (color/colour) or colors (primary_color/secondary_color/tertiary_color), not both." ) - - colors_payload: Dict[str, Any]= {} + + colors_payload: Dict[str, Any] = {} if primary_color is not MISSING: if primary_color is None: colors_payload['primary_color'] = None @@ -3763,7 +3745,7 @@ async def create_role( if colors_payload: fields['colors'] = colors_payload - + if not colors_payload: actual_colour = colour or color or Colour.default() if isinstance(actual_colour, int): diff --git a/discord/role.py b/discord/role.py index 929d4446b79c..e3ea79a31381 100644 --- a/discord/role.py +++ b/discord/role.py @@ -224,7 +224,7 @@ class Role(Hashable): '_state', '_primary_color', '_secondary_color', - '_tertiary_color', + '_tertiary_color', ) def __init__(self, *, guild: Guild, state: ConnectionState, data: RolePayload): @@ -344,6 +344,7 @@ def secondary_color(self) -> Optional[Colour]: def tertiary_color(self) -> Optional[Colour]: """Optional[:class:`Colour`]: The role's tertiary color.""" return Colour(self._tertiary_color) if self._tertiary_color is not None else None + @property def permissions(self) -> Permissions: """:class:`Permissions`: Returns the role's permissions.""" @@ -552,16 +553,12 @@ async def edit( payload['mentionable'] = mentionable solid_color_used = color is not MISSING or colour is not MISSING - colors_used = ( - primary_color is not MISSING or - secondary_color is not MISSING or - tertiary_color is not MISSING - ) + colors_used = primary_color is not MISSING or secondary_color is not MISSING or tertiary_color is not MISSING if solid_color_used and colors_used: raise TypeError( "You must choose either only solid color (color/colour) or colors (primary_color/secondary_color/tertiary_color), not both." ) - + colors_payload: Dict[str, Any] = {} if primary_color is not MISSING: if primary_color is None: @@ -586,25 +583,21 @@ async def edit( colors_payload['tertiary_color'] = tertiary_color.value if colors_payload: payload['colors'] = colors_payload - + data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) return Role(guild=self.guild, data=data, state=self._state) @overload - async def move(self, *, beginning: bool, offset: int = ..., reason: Optional[str] = ...): - ... + async def move(self, *, beginning: bool, offset: int = ..., reason: Optional[str] = ...): ... @overload - async def move(self, *, end: bool, offset: int = ..., reason: Optional[str] = ...): - ... + async def move(self, *, end: bool, offset: int = ..., reason: Optional[str] = ...): ... @overload - async def move(self, *, above: Role, offset: int = ..., reason: Optional[str] = ...): - ... + async def move(self, *, above: Role, offset: int = ..., reason: Optional[str] = ...): ... @overload - async def move(self, *, below: Role, offset: int = ..., reason: Optional[str] = ...): - ... + async def move(self, *, below: Role, offset: int = ..., reason: Optional[str] = ...): ... async def move( self, From e2303847cf39fcf3db28e197705b5be60c7ae4a1 Mon Sep 17 00:00:00 2001 From: makerze <99765898+makerze@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:43:14 +0100 Subject: [PATCH 27/40] black format --- discord/types/guild.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/discord/types/guild.py b/discord/types/guild.py index 7ac90b89ea53..a001500f263a 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -113,8 +113,7 @@ class _GuildPreviewUnique(TypedDict): approximate_presence_count: int -class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique): - ... +class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique): ... class Guild(_BaseGuildPreview): @@ -164,8 +163,7 @@ class InviteGuild(Guild, total=False): welcome_screen: WelcomeScreen -class GuildWithCounts(Guild, _GuildPreviewUnique): - ... +class GuildWithCounts(Guild, _GuildPreviewUnique): ... class GuildPrune(TypedDict): From 59a35a297080689e36958f3934f83096db20d026 Mon Sep 17 00:00:00 2001 From: makerze <99765898+makerze@users.noreply.github.com> Date: Fri, 20 Jun 2025 22:24:06 +0100 Subject: [PATCH 28/40] black format --- discord/guild.py | 42 ++++++++++++++++++++++++++++-------------- discord/role.py | 12 ++++++++---- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 278b78cd3213..5e747799401b 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1293,7 +1293,8 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, TextChannelPayload]: ... + ) -> Coroutine[Any, Any, TextChannelPayload]: + ... @overload def _create_channel( @@ -1303,7 +1304,8 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, VoiceChannelPayload]: ... + ) -> Coroutine[Any, Any, VoiceChannelPayload]: + ... @overload def _create_channel( @@ -1313,7 +1315,8 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, StageChannelPayload]: ... + ) -> Coroutine[Any, Any, StageChannelPayload]: + ... @overload def _create_channel( @@ -1323,7 +1326,8 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, CategoryChannelPayload]: ... + ) -> Coroutine[Any, Any, CategoryChannelPayload]: + ... @overload def _create_channel( @@ -1333,7 +1337,8 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, NewsChannelPayload]: ... + ) -> Coroutine[Any, Any, NewsChannelPayload]: + ... @overload def _create_channel( @@ -1343,7 +1348,8 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: ... + ) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: + ... @overload def _create_channel( @@ -1353,7 +1359,8 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, ForumChannelPayload]: ... + ) -> Coroutine[Any, Any, ForumChannelPayload]: + ... @overload def _create_channel( @@ -1363,7 +1370,8 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, GuildChannelPayload]: ... + ) -> Coroutine[Any, Any, GuildChannelPayload]: + ... def _create_channel( self, @@ -3194,7 +3202,8 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: ... + ) -> ScheduledEvent: + ... @overload async def create_scheduled_event( @@ -3209,7 +3218,8 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: ... + ) -> ScheduledEvent: + ... @overload async def create_scheduled_event( @@ -3223,7 +3233,8 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: ... + ) -> ScheduledEvent: + ... @overload async def create_scheduled_event( @@ -3237,7 +3248,8 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: ... + ) -> ScheduledEvent: + ... async def create_scheduled_event( self, @@ -3605,7 +3617,8 @@ async def create_role( primary_color: Union[Colour, int, None] = ..., secondary_color: Union[Colour, int, None] = ..., tertiary_color: Union[Colour, int, None] = ..., - ) -> Role: ... + ) -> Role: + ... @overload async def create_role( @@ -3621,7 +3634,8 @@ async def create_role( primary_color: Union[Colour, int, None] = ..., secondary_color: Union[Colour, int, None] = ..., tertiary_color: Union[Colour, int, None] = ..., - ) -> Role: ... + ) -> Role: + ... async def create_role( self, diff --git a/discord/role.py b/discord/role.py index e3ea79a31381..2940f2beea16 100644 --- a/discord/role.py +++ b/discord/role.py @@ -588,16 +588,20 @@ async def edit( return Role(guild=self.guild, data=data, state=self._state) @overload - async def move(self, *, beginning: bool, offset: int = ..., reason: Optional[str] = ...): ... + async def move(self, *, beginning: bool, offset: int = ..., reason: Optional[str] = ...): + ... @overload - async def move(self, *, end: bool, offset: int = ..., reason: Optional[str] = ...): ... + async def move(self, *, end: bool, offset: int = ..., reason: Optional[str] = ...): + ... @overload - async def move(self, *, above: Role, offset: int = ..., reason: Optional[str] = ...): ... + async def move(self, *, above: Role, offset: int = ..., reason: Optional[str] = ...): + ... @overload - async def move(self, *, below: Role, offset: int = ..., reason: Optional[str] = ...): ... + async def move(self, *, below: Role, offset: int = ..., reason: Optional[str] = ...): + ... async def move( self, From c8542c411d374907dcf6a7f265a4e546a60ccd21 Mon Sep 17 00:00:00 2001 From: makerze <99765898+makerze@users.noreply.github.com> Date: Fri, 20 Jun 2025 22:36:09 +0100 Subject: [PATCH 29/40] format --- discord/types/guild.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/types/guild.py b/discord/types/guild.py index a001500f263a..7ac90b89ea53 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -113,7 +113,8 @@ class _GuildPreviewUnique(TypedDict): approximate_presence_count: int -class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique): ... +class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique): + ... class Guild(_BaseGuildPreview): @@ -163,7 +164,8 @@ class InviteGuild(Guild, total=False): welcome_screen: WelcomeScreen -class GuildWithCounts(Guild, _GuildPreviewUnique): ... +class GuildWithCounts(Guild, _GuildPreviewUnique): + ... class GuildPrune(TypedDict): From a62b25c6c00fd8de060703eec0cd6e6599fe1291 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Sat, 21 Jun 2025 03:56:36 +0530 Subject: [PATCH 30/40] Add missing attributes in AppCommandChannel --- discord/app_commands/models.py | 69 +++++++++++++++++++++++++++++++++- discord/types/interactions.py | 14 +++++-- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/discord/app_commands/models.py b/discord/app_commands/models.py index dcf70d41e0a4..5851e7d8cce1 100644 --- a/discord/app_commands/models.py +++ b/discord/app_commands/models.py @@ -26,7 +26,7 @@ from datetime import datetime from .errors import MissingApplicationID -from ..flags import AppCommandContext, AppInstallationType +from ..flags import AppCommandContext, AppInstallationType, ChannelFlags from .translator import TranslationContextLocation, TranslationContext, locale_str, Translator from ..permissions import Permissions from ..enums import ( @@ -575,6 +575,35 @@ class AppCommandChannel(Hashable): the application command in that channel. guild_id: :class:`int` The guild ID this channel belongs to. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + + .. versionadded:: 2.6 + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it doesn't exist. + + .. versionadded:: 2.6 + position: :class:`int` + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. + + .. versionadded:: 2.6 + last_message_id: Optional[:class:`int`] + The last message ID of the message sent to this channel. It may + *not* point to an existing or valid message. + + .. versionadded:: 2.6 + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in this channel. A value of ``0`` denotes that it is disabled. + Bots and users with :attr:`~discord.Permissions.manage_channels` or + :attr:`~discord.Permissions.manage_messages` bypass slowmode. + + .. versionadded:: 2.6 + nsfw: :class:`bool` + If the channel is marked as "not safe for work" or "age restricted". + + .. versionadded:: 2.6 """ __slots__ = ( @@ -583,6 +612,14 @@ class AppCommandChannel(Hashable): 'name', 'permissions', 'guild_id', + 'topic', + 'nsfw', + 'position', + 'category_id', + 'slowmode_delay', + 'last_message_id', + '_last_pin', + '_flags', '_state', ) @@ -599,6 +636,14 @@ def __init__( self.type: ChannelType = try_enum(ChannelType, data['type']) self.name: str = data['name'] self.permissions: Permissions = Permissions(int(data['permissions'])) + self.topic: Optional[str] = data.get('topic') + self.position: int = data.get('position') or 0 + self.nsfw: bool = data.get('nsfw') or False + self.category_id: Optional[int] = _get_as_snowflake(data, 'parent_id') + self.slowmode_delay: int = data.get('rate_limit_per_user') or 0 + self.last_message_id: Optional[int] = _get_as_snowflake(data, 'last_message_id') + self._last_pin: Optional[datetime] = parse_time(data.get('last_pin_timestamp')) + self._flags: int = data.get('flags', 0) def __str__(self) -> str: return self.name @@ -611,6 +656,28 @@ def guild(self) -> Optional[Guild]: """Optional[:class:`~discord.Guild`]: The channel's guild, from cache, if found.""" return self._state._get_guild(self.guild_id) + @property + def flags(self) -> ChannelFlags: + """:class:`~discord.ChannelFlags`: The flags associated with this channel object. + + .. versionadded:: 2.6 + """ + return ChannelFlags._from_value(self._flags) + + def is_nsfw(self) -> bool: + """:class:`bool`: Checks if the channel is NSFW. + + .. versionadded:: 2.6 + """ + return self.nsfw + + def is_news(self) -> bool: + """:class:`bool`: Checks if the channel is a news channel. + + .. versionadded:: 2.6 + """ + return self.type == ChannelType.news + def resolve(self) -> Optional[GuildChannel]: """Resolves the application command channel to the appropriate channel from cache if found. diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 3e814b49df6e..464f2445fd1c 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -24,12 +24,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, List, Literal, TypedDict, Union +from typing import TYPE_CHECKING, Dict, List, Literal, TypedDict, Union, Optional from typing_extensions import NotRequired -from .channel import ChannelTypeWithoutThread, ThreadMetadata, GuildChannel, InteractionDMChannel, GroupDMChannel +from .channel import ChannelTypeWithoutThread, GuildChannel, InteractionDMChannel, GroupDMChannel from .sku import Entitlement -from .threads import ThreadType +from .threads import ThreadType, ThreadMetadata from .member import Member from .message import Attachment from .role import Role @@ -64,6 +64,14 @@ class _BasePartialChannel(TypedDict): class PartialChannel(_BasePartialChannel): type: ChannelTypeWithoutThread + topic: NotRequired[str] + position: int + nsfw: bool + flags: int + rate_limit_per_user: int + parent_id: Optional[Snowflake] + last_message_id: Optional[Snowflake] + last_pin_timestamp: NotRequired[str] class PartialThread(_BasePartialChannel): From d26e4d67ad86d1682e04621e14958f20936afe9b Mon Sep 17 00:00:00 2001 From: Mak <99765898+makerze@users.noreply.github.com> Date: Sat, 21 Jun 2025 13:29:14 +0100 Subject: [PATCH 31/40] Update discord/guild.py Co-authored-by: dolfies --- discord/guild.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 0517f527bc57..82a3f3ce0ad2 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3718,9 +3718,8 @@ async def create_role( secondary_color: Union[:class:`Colour`, :class:`int`, None] The secondary color for the role. Requires ``primary_color`` to also be set. tertiary_color: Union[:class:`Colour`, :class:`int`, None] - The tertiary_color color for the role. Used for holographic role. - The holographic preset is: - {"primary_color": 11127295, "secondary_color": 16759788, "tertiary_color": 16761760} + The tertiary color for the role. Can only be used for the holographic role preset, + which is ``(11127295, 16759788, 16761760)`` hoist: :class:`bool` Indicates if the role should be shown separately in the member list. Defaults to ``False``. From f1047a59deecea85facb835db2afe96dd2f8f94a Mon Sep 17 00:00:00 2001 From: makerze <99765898+makerze@users.noreply.github.com> Date: Sat, 21 Jun 2025 15:55:53 +0100 Subject: [PATCH 32/40] Add alias to primary, secondary and tertiary color --- discord/guild.py | 65 ++++++++++++++++------------ discord/role.py | 107 +++++++++++++++++++++++++++++++---------------- 2 files changed, 109 insertions(+), 63 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 82a3f3ce0ad2..19159326907b 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3648,9 +3648,9 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., - primary_color: Union[Colour, int, None] = ..., - secondary_color: Union[Colour, int, None] = ..., - tertiary_color: Union[Colour, int, None] = ..., + primary_colour: Union[Colour, int, None] = ..., + secondary_colour: Union[Colour, int, None] = ..., + tertiary_colour: Union[Colour, int, None] = ..., ) -> Role: ... @@ -3685,6 +3685,9 @@ async def create_role( primary_color: Union[Colour, int, None] = MISSING, secondary_color: Union[Colour, int, None] = MISSING, tertiary_color: Union[Colour, int, None] = MISSING, + primary_colour: Union[Colour, int, None] = MISSING, + secondary_colour: Union[Colour, int, None] = MISSING, + tertiary_colour: Union[Colour, int, None] = MISSING, ) -> Role: """|coro| @@ -3713,12 +3716,12 @@ async def create_role( colour: Union[:class:`Colour`, :class:`int`] The colour for the role. Defaults to :meth:`Colour.default`. This is aliased to ``color`` as well. - primary_color: Union[:class:`Colour`, :class:`int`, None] - The primary color for the role. If provided, must be an integer or :class:`Colour`. - secondary_color: Union[:class:`Colour`, :class:`int`, None] - The secondary color for the role. Requires ``primary_color`` to also be set. - tertiary_color: Union[:class:`Colour`, :class:`int`, None] - The tertiary color for the role. Can only be used for the holographic role preset, + primary_colour: Union[:class:`Colour`, :class:`int`, None] + The primary colour for the role. If provided, must be an integer or :class:`Colour`. + secondary_colour: Union[:class:`Colour`, :class:`int`, None] + The secondary colour for the role. + tertiary_colour: Union[:class:`Colour`, :class:`int`, None] + The tertiary colour for the role. Can only be used for the holographic role preset, which is ``(11127295, 16759788, 16761760)`` hoist: :class:`bool` Indicates if the role should be shown separately in the member list. @@ -3761,34 +3764,44 @@ async def create_role( fields['color'] = actual_colour.value solid_color_used = color is not MISSING or colour is not MISSING - colors_used = primary_color is not MISSING or secondary_color is not MISSING or tertiary_color is not MISSING + colors_used = ( + primary_color is not MISSING + or secondary_color is not MISSING + or tertiary_color is not MISSING + or primary_colour is not MISSING + or secondary_colour is not MISSING + or tertiary_colour is not MISSING + ) if solid_color_used and colors_used: raise TypeError( - "You must choose either only solid color (color/colour) or colors (primary_color/secondary_color/tertiary_color), not both." + "You must choose either only solid colour (color/colour) or colours (primary_colour/secondary_colour/tertiary_colour), not both." ) + actual_primary_colour = primary_colour or primary_color + actual_secondary_colour = secondary_colour or secondary_color + actual_tertiary_colour = tertiary_colour or tertiary_color colors_payload: Dict[str, Any] = {} - if primary_color is not MISSING: - if primary_color is None: + if actual_primary_colour is not MISSING: + if actual_primary_colour is None: colors_payload['primary_color'] = None - elif isinstance(primary_color, int): - colors_payload['primary_color'] = primary_color + elif isinstance(actual_primary_colour, int): + colors_payload['primary_color'] = actual_primary_colour else: - colors_payload['primary_color'] = primary_color.value - if secondary_color is not MISSING: - if secondary_color is None: + colors_payload['primary_color'] = actual_primary_colour.value + if actual_secondary_colour is not MISSING: + if actual_secondary_colour is None: colors_payload['secondary_color'] = None - elif isinstance(secondary_color, int): - colors_payload['secondary_color'] = secondary_color + elif isinstance(actual_secondary_colour, int): + colors_payload['secondary_color'] = actual_secondary_colour else: - colors_payload['secondary_color'] = secondary_color.value - if tertiary_color is not MISSING: - if tertiary_color is None: + colors_payload['secondary_color'] = actual_secondary_colour.value + if actual_tertiary_colour is not MISSING: + if actual_tertiary_colour is None: colors_payload['tertiary_color'] = None - elif isinstance(tertiary_color, int): - colors_payload['tertiary_color'] = tertiary_color + elif isinstance(actual_tertiary_colour, int): + colors_payload['tertiary_color'] = actual_tertiary_colour else: - colors_payload['tertiary_color'] = tertiary_color.value + colors_payload['tertiary_color'] = actual_tertiary_colour.value if colors_payload: fields['colors'] = colors_payload diff --git a/discord/role.py b/discord/role.py index 2940f2beea16..1ec337db6b59 100644 --- a/discord/role.py +++ b/discord/role.py @@ -222,9 +222,9 @@ class Role(Hashable): 'tags', '_flags', '_state', - '_primary_color', - '_secondary_color', - '_tertiary_color', + '_primary_colour', + '_secondary_colour', + '_tertiary_colour', ) def __init__(self, *, guild: Guild, state: ConnectionState, data: RolePayload): @@ -288,9 +288,9 @@ def _update(self, data: RolePayload): self.tags: Optional[RoleTags] self._flags: int = data.get('flags', 0) colors = data.get('colors', {}) - self._primary_color = colors.get('primary_color', None) - self._secondary_color = colors.get('secondary_color', None) - self._tertiary_color = colors.get('tertiary_color', None) + self._primary_colour = colors.get('primary_colour', None) + self._secondary_colour = colors.get('secondary_colour', None) + self._tertiary_colour = colors.get('tertiary_colour', None) try: self.tags = RoleTags(data['tags']) # pyright: ignore[reportTypedDictNotRequiredAccess] @@ -330,20 +330,35 @@ def is_assignable(self) -> bool: me = self.guild.me return not self.is_default() and not self.managed and (me.top_role > self or me.id == self.guild.owner_id) + @property + def primary_colour(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: The role's primary colour.""" + return Colour(self._primary_colour) if self._primary_colour is not None else None + @property def primary_color(self) -> Optional[Colour]: - """Optional[:class:`Colour`]: The role's primary color.""" - return Colour(self._primary_color) if self._primary_color is not None else None + """Optional[:class:`Colour`]: Alias for :attr:`primary_colour`.""" + return self.primary_colour + + @property + def secondary_colour(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: The role's secondary colour.""" + return Colour(self._secondary_colour) if self._secondary_colour is not None else None @property def secondary_color(self) -> Optional[Colour]: - """Optional[:class:`Colour`]: The role's secondary color.""" - return Colour(self._secondary_color) if self._secondary_color is not None else None + """Optional[:class:`Colour`]: Alias for :attr:`secondary_colour`.""" + return self.secondary_colour + + @property + def tertiary_colour(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: The role's tertiary colour.""" + return Colour(self._tertiary_colour) if self._tertiary_colour is not None else None @property def tertiary_color(self) -> Optional[Colour]: - """Optional[:class:`Colour`]: The role's tertiary color.""" - return Colour(self._tertiary_color) if self._tertiary_color is not None else None + """Optional[:class:`Colour`]: Alias for :attr:`tertiary_colour`.""" + return self.tertiary_colour @property def permissions(self) -> Permissions: @@ -450,6 +465,9 @@ async def edit( primary_color: Union[Colour, int, None] = MISSING, secondary_color: Union[Colour, int, None] = MISSING, tertiary_color: Union[Colour, int, None] = MISSING, + primary_colour: Union[Colour, int, None] = MISSING, + secondary_colour: Union[Colour, int, None] = MISSING, + tertiary_colour: Union[Colour, int, None] = MISSING, ) -> Optional[Role]: """|coro| @@ -480,6 +498,13 @@ async def edit( The new permissions to change to. colour: Union[:class:`Colour`, :class:`int`] The new colour to change to. (aliased to color as well) + primary_colour: Union[:class:`Colour`, :class:`int`, None] + The new primary colour for the role. If provided, must be an integer or :class:`Colour`. + secondary_colour: Union[:class:`Colour`, :class:`int`, None] + The new secondary colour for the role. + tertiary_colour: Union[:class:`Colour`, :class:`int`, None] + The new tertiary colour for the role. Can only be used for the holographic role preset, + which is ``(11127295, 16759788, 16761760)`` hoist: :class:`bool` Indicates if the role should be shown separately in the member list. display_icon: Optional[Union[:class:`bytes`, :class:`str`]] @@ -495,14 +520,6 @@ async def edit( position or it will fail. reason: Optional[:class:`str`] The reason for editing this role. Shows up on the audit log. - primary_color: Union[:class:`Colour`, :class:`int`, None] - The primary color for the role. If provided, must be an integer or :class:`Colour`. - secondary_color: Union[:class:`Colour`, :class:`int`, None] - The secondary color for the role. - tertiary_color: Union[:class:`Colour`, :class:`int`, None] - The tertiary_color color for the role. Used for holographic role. - The holographic preset is: - {"primary_color": 11127295, "secondary_color": 16759788, "tertiary_color": 16761760} Raises ------- @@ -553,34 +570,50 @@ async def edit( payload['mentionable'] = mentionable solid_color_used = color is not MISSING or colour is not MISSING - colors_used = primary_color is not MISSING or secondary_color is not MISSING or tertiary_color is not MISSING + colors_used = ( + primary_color is not MISSING + or secondary_color is not MISSING + or tertiary_color is not MISSING + or primary_colour is not MISSING + or secondary_colour is not MISSING + or tertiary_colour is not MISSING + ) if solid_color_used and colors_used: raise TypeError( - "You must choose either only solid color (color/colour) or colors (primary_color/secondary_color/tertiary_color), not both." + "You must choose either only solid colour (color/colour) or colours (primary_colour/secondary_colour/tertiary_colour), not both." ) - colors_payload: Dict[str, Any] = {} if primary_color is not MISSING: - if primary_color is None: + primary_colour = primary_color + + if secondary_color is not MISSING: + secondary_colour = secondary_color + + if tertiary_color is not MISSING: + tertiary_colour = tertiary_color + + colors_payload: Dict[str, Any] = {} + if primary_colour is not MISSING: + if primary_colour is None: colors_payload['primary_color'] = None - elif isinstance(primary_color, int): - colors_payload['primary_color'] = primary_color + elif isinstance(primary_colour, int): + colors_payload['primary_color'] = primary_colour else: - colors_payload['primary_color'] = primary_color.value - if secondary_color is not MISSING: - if secondary_color is None: + colors_payload['primary_color'] = primary_colour.value + if secondary_colour is not MISSING: + if secondary_colour is None: colors_payload['secondary_color'] = None - elif isinstance(secondary_color, int): - colors_payload['secondary_color'] = secondary_color + elif isinstance(secondary_colour, int): + colors_payload['secondary_color'] = secondary_colour else: - colors_payload['secondary_color'] = secondary_color.value - if tertiary_color is not MISSING: - if tertiary_color is None: + colors_payload['secondary_color'] = secondary_colour.value + if tertiary_colour is not MISSING: + if tertiary_colour is None: colors_payload['tertiary_color'] = None - elif isinstance(tertiary_color, int): - colors_payload['tertiary_color'] = tertiary_color + elif isinstance(tertiary_colour, int): + colors_payload['tertiary_color'] = tertiary_colour else: - colors_payload['tertiary_color'] = tertiary_color.value + colors_payload['tertiary_color'] = tertiary_colour.value if colors_payload: payload['colors'] = colors_payload From 8bcc0c7ad94515f90b483fff875f65c0d28cf2d6 Mon Sep 17 00:00:00 2001 From: Mak <99765898+makerze@users.noreply.github.com> Date: Mon, 23 Jun 2025 01:05:30 +0100 Subject: [PATCH 33/40] Update discord/guild.py Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> --- discord/guild.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 19159326907b..e3fdf386a149 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3763,19 +3763,6 @@ async def create_role( else: fields['color'] = actual_colour.value - solid_color_used = color is not MISSING or colour is not MISSING - colors_used = ( - primary_color is not MISSING - or secondary_color is not MISSING - or tertiary_color is not MISSING - or primary_colour is not MISSING - or secondary_colour is not MISSING - or tertiary_colour is not MISSING - ) - if solid_color_used and colors_used: - raise TypeError( - "You must choose either only solid colour (color/colour) or colours (primary_colour/secondary_colour/tertiary_colour), not both." - ) actual_primary_colour = primary_colour or primary_color actual_secondary_colour = secondary_colour or secondary_color From 864437464d5f0b3301e47e5a39641eed9697501f Mon Sep 17 00:00:00 2001 From: Mak <99765898+makerze@users.noreply.github.com> Date: Mon, 23 Jun 2025 01:09:40 +0100 Subject: [PATCH 34/40] Apply suggestions from code review Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> --- discord/guild.py | 41 +++++++++++------------------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index e3fdf386a149..3cfbce662dab 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3648,7 +3648,6 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., - primary_colour: Union[Colour, int, None] = ..., secondary_colour: Union[Colour, int, None] = ..., tertiary_colour: Union[Colour, int, None] = ..., ) -> Role: @@ -3665,7 +3664,6 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., - primary_color: Union[Colour, int, None] = ..., secondary_color: Union[Colour, int, None] = ..., tertiary_color: Union[Colour, int, None] = ..., ) -> Role: @@ -3682,10 +3680,8 @@ async def create_role( display_icon: Union[bytes, str] = MISSING, mentionable: bool = MISSING, reason: Optional[str] = None, - primary_color: Union[Colour, int, None] = MISSING, secondary_color: Union[Colour, int, None] = MISSING, tertiary_color: Union[Colour, int, None] = MISSING, - primary_colour: Union[Colour, int, None] = MISSING, secondary_colour: Union[Colour, int, None] = MISSING, tertiary_colour: Union[Colour, int, None] = MISSING, ) -> Role: @@ -3716,8 +3712,6 @@ async def create_role( colour: Union[:class:`Colour`, :class:`int`] The colour for the role. Defaults to :meth:`Colour.default`. This is aliased to ``color`` as well. - primary_colour: Union[:class:`Colour`, :class:`int`, None] - The primary colour for the role. If provided, must be an integer or :class:`Colour`. secondary_colour: Union[:class:`Colour`, :class:`int`, None] The secondary colour for the role. tertiary_colour: Union[:class:`Colour`, :class:`int`, None] @@ -3764,41 +3758,28 @@ async def create_role( fields['color'] = actual_colour.value - actual_primary_colour = primary_colour or primary_color actual_secondary_colour = secondary_colour or secondary_color actual_tertiary_colour = tertiary_colour or tertiary_color - colors_payload: Dict[str, Any] = {} - if actual_primary_colour is not MISSING: - if actual_primary_colour is None: - colors_payload['primary_color'] = None - elif isinstance(actual_primary_colour, int): - colors_payload['primary_color'] = actual_primary_colour - else: - colors_payload['primary_color'] = actual_primary_colour.value + colours = { + 'primary_color': fields['color'], + } + if actual_secondary_colour is not MISSING: if actual_secondary_colour is None: - colors_payload['secondary_color'] = None + colours['secondary_color'] = None elif isinstance(actual_secondary_colour, int): - colors_payload['secondary_color'] = actual_secondary_colour + colours['secondary_color'] = actual_secondary_colour else: - colors_payload['secondary_color'] = actual_secondary_colour.value + colours['secondary_color'] = actual_secondary_colour.value if actual_tertiary_colour is not MISSING: if actual_tertiary_colour is None: - colors_payload['tertiary_color'] = None + colours['tertiary_color'] = None elif isinstance(actual_tertiary_colour, int): - colors_payload['tertiary_color'] = actual_tertiary_colour + colours['tertiary_color'] = actual_tertiary_colour else: - colors_payload['tertiary_color'] = actual_tertiary_colour.value + colours['tertiary_color'] = actual_tertiary_colour.value - if colors_payload: - fields['colors'] = colors_payload - - if not colors_payload: - actual_colour = colour or color or Colour.default() - if isinstance(actual_colour, int): - fields['color'] = actual_colour - else: - fields['color'] = actual_colour.value + fields['colors'] = colours if hoist is not MISSING: fields['hoist'] = hoist From 8b5895cffc370d918175413bdf69b0fc6235685f Mon Sep 17 00:00:00 2001 From: Mak <99765898+makerze@users.noreply.github.com> Date: Mon, 23 Jun 2025 01:15:27 +0100 Subject: [PATCH 35/40] Apply suggestions from code review Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> --- discord/role.py | 66 +++++++++++++------------------------------------ 1 file changed, 17 insertions(+), 49 deletions(-) diff --git a/discord/role.py b/discord/role.py index 1ec337db6b59..9a119a79af23 100644 --- a/discord/role.py +++ b/discord/role.py @@ -462,10 +462,8 @@ async def edit( mentionable: bool = MISSING, position: int = MISSING, reason: Optional[str] = MISSING, - primary_color: Union[Colour, int, None] = MISSING, secondary_color: Union[Colour, int, None] = MISSING, tertiary_color: Union[Colour, int, None] = MISSING, - primary_colour: Union[Colour, int, None] = MISSING, secondary_colour: Union[Colour, int, None] = MISSING, tertiary_colour: Union[Colour, int, None] = MISSING, ) -> Optional[Role]: @@ -498,8 +496,6 @@ async def edit( The new permissions to change to. colour: Union[:class:`Colour`, :class:`int`] The new colour to change to. (aliased to color as well) - primary_colour: Union[:class:`Colour`, :class:`int`, None] - The new primary colour for the role. If provided, must be an integer or :class:`Colour`. secondary_colour: Union[:class:`Colour`, :class:`int`, None] The new secondary colour for the role. tertiary_colour: Union[:class:`Colour`, :class:`int`, None] @@ -569,54 +565,26 @@ async def edit( if mentionable is not MISSING: payload['mentionable'] = mentionable - solid_color_used = color is not MISSING or colour is not MISSING - colors_used = ( - primary_color is not MISSING - or secondary_color is not MISSING - or tertiary_color is not MISSING - or primary_colour is not MISSING - or secondary_colour is not MISSING - or tertiary_colour is not MISSING - ) - if solid_color_used and colors_used: - raise TypeError( - "You must choose either only solid colour (color/colour) or colours (primary_colour/secondary_colour/tertiary_colour), not both." - ) - - if primary_color is not MISSING: - primary_colour = primary_color - - if secondary_color is not MISSING: - secondary_colour = secondary_color - - if tertiary_color is not MISSING: - tertiary_colour = tertiary_color - - colors_payload: Dict[str, Any] = {} - if primary_colour is not MISSING: - if primary_colour is None: - colors_payload['primary_color'] = None - elif isinstance(primary_colour, int): - colors_payload['primary_color'] = primary_colour - else: - colors_payload['primary_color'] = primary_colour.value - if secondary_colour is not MISSING: - if secondary_colour is None: - colors_payload['secondary_color'] = None - elif isinstance(secondary_colour, int): - colors_payload['secondary_color'] = secondary_colour + colours = { + 'primary_color': payload['color'], + } + + if actual_secondary_colour is not MISSING: + if actual_secondary_colour is None: + colours['secondary_color'] = None + elif isinstance(actual_secondary_colour, int): + colours['secondary_color'] = actual_secondary_colour else: - colors_payload['secondary_color'] = secondary_colour.value - if tertiary_colour is not MISSING: - if tertiary_colour is None: - colors_payload['tertiary_color'] = None - elif isinstance(tertiary_colour, int): - colors_payload['tertiary_color'] = tertiary_colour + colours['secondary_color'] = actual_secondary_colour.value + if actual_tertiary_colour is not MISSING: + if actual_tertiary_colour is None: + colours['tertiary_color'] = None + elif isinstance(actual_tertiary_colour, int): + colours['tertiary_color'] = actual_tertiary_colour else: - colors_payload['tertiary_color'] = tertiary_colour.value - if colors_payload: - payload['colors'] = colors_payload + colours['tertiary_color'] = actual_tertiary_colour.value + payload['colors'] = colours data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) return Role(guild=self.guild, data=data, state=self._state) From ee963d05a682ce58d7a995dc558527791717e7fb Mon Sep 17 00:00:00 2001 From: makerze <99765898+makerze@users.noreply.github.com> Date: Mon, 23 Jun 2025 02:11:00 +0100 Subject: [PATCH 36/40] Fix: color=primary_color and add types --- discord/guild.py | 16 ++++++---------- discord/role.py | 17 ++++------------- discord/types/role.py | 2 ++ 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 3cfbce662dab..7d9e7a4734e2 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3757,24 +3757,20 @@ async def create_role( else: fields['color'] = actual_colour.value - - actual_secondary_colour = secondary_colour or secondary_color + actual_secondary_colour = secondary_colour or secondary_color or Colour.default() actual_tertiary_colour = tertiary_colour or tertiary_color colours = { 'primary_color': fields['color'], } - + if actual_secondary_colour is not MISSING: - if actual_secondary_colour is None: - colours['secondary_color'] = None - elif isinstance(actual_secondary_colour, int): + if isinstance(actual_secondary_colour, int): colours['secondary_color'] = actual_secondary_colour else: colours['secondary_color'] = actual_secondary_colour.value - if actual_tertiary_colour is not MISSING: - if actual_tertiary_colour is None: - colours['tertiary_color'] = None - elif isinstance(actual_tertiary_colour, int): + + if actual_tertiary_colour is not MISSING and actual_tertiary_colour is not None: + if isinstance(actual_tertiary_colour, int): colours['tertiary_color'] = actual_tertiary_colour else: colours['tertiary_color'] = actual_tertiary_colour.value diff --git a/discord/role.py b/discord/role.py index 9a119a79af23..ed0bdf4307b1 100644 --- a/discord/role.py +++ b/discord/role.py @@ -222,7 +222,6 @@ class Role(Hashable): 'tags', '_flags', '_state', - '_primary_colour', '_secondary_colour', '_tertiary_colour', ) @@ -288,7 +287,6 @@ def _update(self, data: RolePayload): self.tags: Optional[RoleTags] self._flags: int = data.get('flags', 0) colors = data.get('colors', {}) - self._primary_colour = colors.get('primary_colour', None) self._secondary_colour = colors.get('secondary_colour', None) self._tertiary_colour = colors.get('tertiary_colour', None) @@ -330,16 +328,6 @@ def is_assignable(self) -> bool: me = self.guild.me return not self.is_default() and not self.managed and (me.top_role > self or me.id == self.guild.owner_id) - @property - def primary_colour(self) -> Optional[Colour]: - """Optional[:class:`Colour`]: The role's primary colour.""" - return Colour(self._primary_colour) if self._primary_colour is not None else None - - @property - def primary_color(self) -> Optional[Colour]: - """Optional[:class:`Colour`]: Alias for :attr:`primary_colour`.""" - return self.primary_colour - @property def secondary_colour(self) -> Optional[Colour]: """Optional[:class:`Colour`]: The role's secondary colour.""" @@ -568,7 +556,10 @@ async def edit( colours = { 'primary_color': payload['color'], } - + + actual_secondary_colour = secondary_colour or secondary_color + actual_tertiary_colour = tertiary_colour or tertiary_color + if actual_secondary_colour is not MISSING: if actual_secondary_colour is None: colours['secondary_color'] = None diff --git a/discord/types/role.py b/discord/types/role.py index d32de88032de..9614aa07a19e 100644 --- a/discord/types/role.py +++ b/discord/types/role.py @@ -34,6 +34,8 @@ class Role(TypedDict): id: Snowflake name: str color: int + secondary_color: NotRequired[Optional[int]] + tertiary_color: NotRequired[Optional[int]] hoist: bool position: int permissions: str From 95c4744a304297849e75dbfde6489b06d8976ae8 Mon Sep 17 00:00:00 2001 From: makerze <99765898+makerze@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:29:40 +0100 Subject: [PATCH 37/40] Fix: types and broken none --- discord/guild.py | 35 ++++++++++++++++++++--------------- discord/role.py | 18 +++++++++--------- discord/types/guild.py | 1 + discord/types/role.py | 9 +++++++-- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 7d9e7a4734e2..ff6d53516add 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3648,8 +3648,8 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., - secondary_colour: Union[Colour, int, None] = ..., - tertiary_colour: Union[Colour, int, None] = ..., + secondary_colour: Optional[Union[Colour, int]] = ..., + tertiary_colour: Optional[Union[Colour, int]] = ..., ) -> Role: ... @@ -3664,8 +3664,8 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., - secondary_color: Union[Colour, int, None] = ..., - tertiary_color: Union[Colour, int, None] = ..., + secondary_color: Optional[Union[Colour, int]] = ..., + tertiary_color: Optional[Union[Colour, int]] = ..., ) -> Role: ... @@ -3680,10 +3680,10 @@ async def create_role( display_icon: Union[bytes, str] = MISSING, mentionable: bool = MISSING, reason: Optional[str] = None, - secondary_color: Union[Colour, int, None] = MISSING, - tertiary_color: Union[Colour, int, None] = MISSING, - secondary_colour: Union[Colour, int, None] = MISSING, - tertiary_colour: Union[Colour, int, None] = MISSING, + secondary_color: Optional[Union[Colour, int]] = MISSING, + tertiary_color: Optional[Union[Colour, int]] = MISSING, + secondary_colour: Optional[Union[Colour, int]] = MISSING, + tertiary_colour: Optional[Union[Colour, int]] = MISSING, ) -> Role: """|coro| @@ -3712,9 +3712,9 @@ async def create_role( colour: Union[:class:`Colour`, :class:`int`] The colour for the role. Defaults to :meth:`Colour.default`. This is aliased to ``color`` as well. - secondary_colour: Union[:class:`Colour`, :class:`int`, None] + secondary_colour: Optional[Union[:class:`Colour`, :class:`int`]] The secondary colour for the role. - tertiary_colour: Union[:class:`Colour`, :class:`int`, None] + tertiary_colour: Optional[Union[:class:`Colour`, :class:`int`]] The tertiary colour for the role. Can only be used for the holographic role preset, which is ``(11127295, 16759788, 16761760)`` hoist: :class:`bool` @@ -3757,20 +3757,25 @@ async def create_role( else: fields['color'] = actual_colour.value - actual_secondary_colour = secondary_colour or secondary_color or Colour.default() + actual_secondary_colour = secondary_colour or secondary_color actual_tertiary_colour = tertiary_colour or tertiary_color - colours = { + + colours: Dict[str, Any] = { 'primary_color': fields['color'], } if actual_secondary_colour is not MISSING: - if isinstance(actual_secondary_colour, int): + if actual_secondary_colour is None: + colours['secondary_color'] = None + elif isinstance(actual_secondary_colour, int): colours['secondary_color'] = actual_secondary_colour else: colours['secondary_color'] = actual_secondary_colour.value - if actual_tertiary_colour is not MISSING and actual_tertiary_colour is not None: - if isinstance(actual_tertiary_colour, int): + if actual_tertiary_colour is not MISSING: + if actual_tertiary_colour is None: + colours['tertiary_color'] = None + elif isinstance(actual_tertiary_colour, int): colours['tertiary_color'] = actual_tertiary_colour else: colours['tertiary_color'] = actual_tertiary_colour.value diff --git a/discord/role.py b/discord/role.py index ed0bdf4307b1..7f51782c5488 100644 --- a/discord/role.py +++ b/discord/role.py @@ -287,8 +287,8 @@ def _update(self, data: RolePayload): self.tags: Optional[RoleTags] self._flags: int = data.get('flags', 0) colors = data.get('colors', {}) - self._secondary_colour = colors.get('secondary_colour', None) - self._tertiary_colour = colors.get('tertiary_colour', None) + self._secondary_colour = colors.get('secondary_color', None) + self._tertiary_colour = colors.get('tertiary_color', None) try: self.tags = RoleTags(data['tags']) # pyright: ignore[reportTypedDictNotRequiredAccess] @@ -450,10 +450,10 @@ async def edit( mentionable: bool = MISSING, position: int = MISSING, reason: Optional[str] = MISSING, - secondary_color: Union[Colour, int, None] = MISSING, - tertiary_color: Union[Colour, int, None] = MISSING, - secondary_colour: Union[Colour, int, None] = MISSING, - tertiary_colour: Union[Colour, int, None] = MISSING, + secondary_color: Optional[Union[Colour, int]] = MISSING, + tertiary_color: Optional[Union[Colour, int]] = MISSING, + secondary_colour: Optional[Union[Colour, int]] = MISSING, + tertiary_colour: Optional[Union[Colour, int]] = MISSING, ) -> Optional[Role]: """|coro| @@ -484,9 +484,9 @@ async def edit( The new permissions to change to. colour: Union[:class:`Colour`, :class:`int`] The new colour to change to. (aliased to color as well) - secondary_colour: Union[:class:`Colour`, :class:`int`, None] + secondary_colour: Optional[Union[:class:`Colour`, :class:`int`]] The new secondary colour for the role. - tertiary_colour: Union[:class:`Colour`, :class:`int`, None] + tertiary_colour: Optional[Union[:class:`Colour`, :class:`int`]] The new tertiary colour for the role. Can only be used for the holographic role preset, which is ``(11127295, 16759788, 16761760)`` hoist: :class:`bool` @@ -553,7 +553,7 @@ async def edit( if mentionable is not MISSING: payload['mentionable'] = mentionable - colours = { + colours: Dict[str, Any] = { 'primary_color': payload['color'], } diff --git a/discord/types/guild.py b/discord/types/guild.py index 7ac90b89ea53..0e328fed23f5 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -90,6 +90,7 @@ class IncidentData(TypedDict): 'VERIFIED', 'VIP_REGIONS', 'WELCOME_SCREEN_ENABLED', + 'ENHANCED_ROLE_COLORS', 'RAID_ALERTS_DISABLED', 'SOUNDBOARD', 'MORE_SOUNDBOARD', diff --git a/discord/types/role.py b/discord/types/role.py index 9614aa07a19e..dabd1c1cfc30 100644 --- a/discord/types/role.py +++ b/discord/types/role.py @@ -30,12 +30,17 @@ from .snowflake import Snowflake +class RoleColours(TypedDict): + primary_color: int + secondary_color: Optional[int] + tertiary_color: Optional[int] + + class Role(TypedDict): id: Snowflake name: str color: int - secondary_color: NotRequired[Optional[int]] - tertiary_color: NotRequired[Optional[int]] + colors: RoleColours hoist: bool position: int permissions: str From 98e36682149fa4abd56fa837f8ff3433e240228f Mon Sep 17 00:00:00 2001 From: makerze <99765898+makerze@users.noreply.github.com> Date: Sun, 6 Jul 2025 02:07:36 +0100 Subject: [PATCH 38/40] refactor: Colour assigns only primary_color and added version strings --- discord/guild.py | 17 +++++++++++------ discord/role.py | 41 +++++++++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index ff6d53516add..1d3679ad3db2 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3703,6 +3703,13 @@ async def create_role( This function will now raise :exc:`TypeError` instead of ``InvalidArgument``. + .. versionadded:: 2.6 + The ``secondary_color``, ``tertiary_color``, ``secondary_colour``, and ``tertiary_colour`` keyword-only parameters were added. + + .. versionchanged:: 2.6 + The ``colour`` and ``color`` parameters now set the role's primary color. + + Parameters ----------- name: :class:`str` @@ -3751,19 +3758,17 @@ async def create_role( else: fields['permissions'] = '0' + colours: Dict[str, Any] = {} + actual_colour = colour or color or Colour.default() if isinstance(actual_colour, int): - fields['color'] = actual_colour + colours['primary_color'] = actual_colour else: - fields['color'] = actual_colour.value + colours['primary_color'] = actual_colour.value actual_secondary_colour = secondary_colour or secondary_color actual_tertiary_colour = tertiary_colour or tertiary_color - colours: Dict[str, Any] = { - 'primary_color': fields['color'], - } - if actual_secondary_colour is not MISSING: if actual_secondary_colour is None: colours['secondary_color'] = None diff --git a/discord/role.py b/discord/role.py index 7f51782c5488..881864ad8bdb 100644 --- a/discord/role.py +++ b/discord/role.py @@ -275,10 +275,11 @@ def __ge__(self, other: object) -> bool: return not r def _update(self, data: RolePayload): + colors = data.get('colors', {}) self.name: str = data['name'] self._permissions: int = int(data.get('permissions', 0)) self.position: int = data.get('position', 0) - self._colour: int = data.get('color', 0) + self._colour: int = colors.get('primary_color', 0) self.hoist: bool = data.get('hoist', False) self._icon: Optional[str] = data.get('icon') self.unicode_emoji: Optional[str] = data.get('unicode_emoji') @@ -286,7 +287,6 @@ def _update(self, data: RolePayload): self.mentionable: bool = data.get('mentionable', False) self.tags: Optional[RoleTags] self._flags: int = data.get('flags', 0) - colors = data.get('colors', {}) self._secondary_colour = colors.get('secondary_color', None) self._tertiary_colour = colors.get('tertiary_color', None) @@ -330,22 +330,30 @@ def is_assignable(self) -> bool: @property def secondary_colour(self) -> Optional[Colour]: - """Optional[:class:`Colour`]: The role's secondary colour.""" + """Optional[:class:`Colour`]: The role's secondary colour. + .. versionadded:: 2.6 + """ return Colour(self._secondary_colour) if self._secondary_colour is not None else None @property def secondary_color(self) -> Optional[Colour]: - """Optional[:class:`Colour`]: Alias for :attr:`secondary_colour`.""" + """Optional[:class:`Colour`]: Alias for :attr:`secondary_colour`. + .. versionadded:: 2.6 + """ return self.secondary_colour @property def tertiary_colour(self) -> Optional[Colour]: - """Optional[:class:`Colour`]: The role's tertiary colour.""" + """Optional[:class:`Colour`]: The role's tertiary colour. + .. versionadded:: 2.6 + """ return Colour(self._tertiary_colour) if self._tertiary_colour is not None else None @property def tertiary_color(self) -> Optional[Colour]: - """Optional[:class:`Colour`]: Alias for :attr:`tertiary_colour`.""" + """Optional[:class:`Colour`]: Alias for :attr:`tertiary_colour`. + .. versionadded:: 2.6 + """ return self.tertiary_colour @property @@ -355,12 +363,12 @@ def permissions(self) -> Permissions: @property def colour(self) -> Colour: - """:class:`Colour`: Returns the role colour. An alias exists under ``color``.""" + """:class:`Colour`: Returns the role's primary colour. An alias exists under ``color``.""" return Colour(self._colour) @property def color(self) -> Colour: - """:class:`Colour`: Returns the role color. An alias exists under ``colour``.""" + """:class:`Colour`: Returns the role's primary colour. An alias exists under ``colour``.""" return self.colour @property @@ -476,6 +484,12 @@ async def edit( This function will now raise :exc:`ValueError` instead of ``InvalidArgument``. + .. versionadded:: 2.6 + The ``secondary_color``, ``tertiary_color``, ``secondary_colour``, and ``tertiary_colour`` keyword-only parameters were added. + + .. versionchanged:: 2.6 + The ``colour`` and ``color`` parameters now set the role's primary color. + Parameters ----------- name: :class:`str` @@ -524,14 +538,17 @@ async def edit( await self._move(position, reason=reason) payload: Dict[str, Any] = {} + + colours: Dict[str, Any] = {} + if color is not MISSING: colour = color if colour is not MISSING: if isinstance(colour, int): - payload['color'] = colour + colours['primary_color'] = colour else: - payload['color'] = colour.value + colours['primary_color'] = colour.value if name is not MISSING: payload['name'] = name @@ -553,10 +570,6 @@ async def edit( if mentionable is not MISSING: payload['mentionable'] = mentionable - colours: Dict[str, Any] = { - 'primary_color': payload['color'], - } - actual_secondary_colour = secondary_colour or secondary_color actual_tertiary_colour = tertiary_colour or tertiary_color From 343130ffce34b4f8670843c95b3fc8d2168ccade Mon Sep 17 00:00:00 2001 From: Mak <99765898+makerze@users.noreply.github.com> Date: Sun, 6 Jul 2025 02:46:20 +0100 Subject: [PATCH 39/40] Apply suggestions from code review Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> --- discord/guild.py | 7 ++++--- discord/role.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 1d3679ad3db2..b03dbbea6445 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3703,9 +3703,6 @@ async def create_role( This function will now raise :exc:`TypeError` instead of ``InvalidArgument``. - .. versionadded:: 2.6 - The ``secondary_color``, ``tertiary_color``, ``secondary_colour``, and ``tertiary_colour`` keyword-only parameters were added. - .. versionchanged:: 2.6 The ``colour`` and ``color`` parameters now set the role's primary color. @@ -3721,9 +3718,13 @@ async def create_role( This is aliased to ``color`` as well. secondary_colour: Optional[Union[:class:`Colour`, :class:`int`]] The secondary colour for the role. + + .. versionadded:: 2.6 tertiary_colour: Optional[Union[:class:`Colour`, :class:`int`]] The tertiary colour for the role. Can only be used for the holographic role preset, which is ``(11127295, 16759788, 16761760)`` + + .. versionadded:: 2.6 hoist: :class:`bool` Indicates if the role should be shown separately in the member list. Defaults to ``False``. diff --git a/discord/role.py b/discord/role.py index 881864ad8bdb..81b4e8cd90b4 100644 --- a/discord/role.py +++ b/discord/role.py @@ -484,9 +484,6 @@ async def edit( This function will now raise :exc:`ValueError` instead of ``InvalidArgument``. - .. versionadded:: 2.6 - The ``secondary_color``, ``tertiary_color``, ``secondary_colour``, and ``tertiary_colour`` keyword-only parameters were added. - .. versionchanged:: 2.6 The ``colour`` and ``color`` parameters now set the role's primary color. @@ -500,9 +497,13 @@ async def edit( The new colour to change to. (aliased to color as well) secondary_colour: Optional[Union[:class:`Colour`, :class:`int`]] The new secondary colour for the role. + + .. versionadded:: 2.6 tertiary_colour: Optional[Union[:class:`Colour`, :class:`int`]] The new tertiary colour for the role. Can only be used for the holographic role preset, which is ``(11127295, 16759788, 16761760)`` + + .. versionadded:: 2.6 hoist: :class:`bool` Indicates if the role should be shown separately in the member list. display_icon: Optional[Union[:class:`bytes`, :class:`str`]] From 70b5a1799675f469bc32c4015a20f66118124874 Mon Sep 17 00:00:00 2001 From: Mak <99765898+makerze@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:32:12 +0100 Subject: [PATCH 40/40] Update discord/role.py Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/role.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 81b4e8cd90b4..acb112519516 100644 --- a/discord/role.py +++ b/discord/role.py @@ -589,7 +589,8 @@ async def edit( else: colours['tertiary_color'] = actual_tertiary_colour.value - payload['colors'] = colours + if colours: + payload['colors'] = colours data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) return Role(guild=self.guild, data=data, state=self._state)