From 1cdc82a31e53b20ff67bf4c3f20eb0441b020214 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sun, 24 Aug 2025 05:53:36 +0200 Subject: [PATCH 01/14] feat: Add support for user.primary_guild Introduces the PrimaryGuild class and its type definition, adds the primary_guild attribute to the User model, and implements asset support for primary guild badges. Updates the changelog to document these additions. --- CHANGELOG.md | 2 + discord/asset.py | 25 ++++++++++++ discord/primary_guild.py | 74 ++++++++++++++++++++++++++++++++++ discord/types/primary_guild.py | 36 +++++++++++++++++ discord/user.py | 5 +++ 5 files changed, 142 insertions(+) create mode 100644 discord/primary_guild.py create mode 100644 discord/types/primary_guild.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d76398448..56c966936b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2818](https://github.com/Pycord-Development/pycord/pull/2818)) - Added `Interaction.attachment_size_limit`. ([#2854](https://github.com/Pycord-Development/pycord/pull/2854)) +- Added `discord.User.primary_guild` and the `PrimaryGuild` class. + ([#2876](https://github.com/Pycord-Development/pycord/pull/2876)) ### Fixed diff --git a/discord/asset.py b/discord/asset.py index 2397a189bd..e66a0d2f1c 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -201,6 +201,31 @@ def _from_avatar_decoration( animated=animated, ) + @classmethod + def _from_user_primary_guild_tag(cls, state, identity_guild_id: int, badge_id: str) -> Asset: + """Creates an Asset for a user's primary guild (tag) badge. + + Parameters + ---------- + state: ConnectionState + The connection state. + identity_guild_id: int + The ID of the guild. + badge_id: str + The badge hash/id. + + Returns + ------- + :class:`Asset` + The primary guild badge asset. + """ + return cls( + state, + url=f"{Asset.BASE}/guild-tag-badges/{identity_guild_id}/{badge_id}.png?size=256", + key=badge_id, + animated=False, + ) + @classmethod def _from_guild_avatar( cls, state, guild_id: int, member_id: int, avatar: str diff --git a/discord/primary_guild.py b/discord/primary_guild.py new file mode 100644 index 0000000000..172df24481 --- /dev/null +++ b/discord/primary_guild.py @@ -0,0 +1,74 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .state import ConnectionState + +from .asset import Asset +from .types.primary_guild import PrimaryGuild as PrimaryGuildPayload + + +class PrimaryGuild: + """ + Represents a Discord Primary Guild. + + .. versionadded:: 2.7 + + Attributes + ---------- + identity_guild_id: int + The ID of the guild. + identity_enabled: :class:`bool` + Whether the primary guild is enabled. + tag: str + The tag of the primary guild. + """ + + def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: + self.identity_guild_id: int = data["identity_guild_id"] + self.identity_enabled: bool | None = data.get("identity_enabled", None) + self.tag: str = data["tag"] + self._badge: str = data["badge"] + self._state: "ConnectionState" = state + + def __repr__(self) -> str: + return f"" + + @property + def badge(self) -> Asset | None: + """Returns the badge asset, if available. + + .. versionadded:: 2.7 + + .. note:: + This information is only available via :meth:`Client.fetch_user`. + """ + if self._badge is None: + return None + return Asset._from_user_primary_guild_tag(self._state, self.identity_guild_id, self._badge) + + +__all__ = ("PrimaryGuild",) diff --git a/discord/types/primary_guild.py b/discord/types/primary_guild.py new file mode 100644 index 0000000000..5ee6386cc3 --- /dev/null +++ b/discord/types/primary_guild.py @@ -0,0 +1,36 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +from typing import TypedDict + +from .snowflake import Snowflake + + +class PrimaryGuild(TypedDict): + identity_guild_id: Snowflake + identity_enabled: bool | None + tag: str + badge: str diff --git a/discord/user.py b/discord/user.py index 368381191c..148e137757 100644 --- a/discord/user.py +++ b/discord/user.py @@ -29,6 +29,7 @@ import discord.abc +from .primary_guild import PrimaryGuild from .asset import Asset from .collectibles import Nameplate from .colour import Colour @@ -78,6 +79,7 @@ class BaseUser(_UserTag): "_avatar_decoration", "_state", "nameplate", + "primary_guild", ) if TYPE_CHECKING: @@ -94,6 +96,7 @@ class BaseUser(_UserTag): _avatar_decoration: dict | None _public_flags: int nameplate: Nameplate | None + primary_guild: PrimaryGuild | None def __init__( self, *, state: ConnectionState, data: UserPayload | PartialUserPayload @@ -151,6 +154,7 @@ def _update(self, data: UserPayload) -> None: self.nameplate = Nameplate(data=nameplate, state=self._state) else: self.nameplate = None + self.primary_guild = data.get("primary_guild", None) self._public_flags = data.get("public_flags", 0) self.bot = data.get("bot", False) self.system = data.get("system", False) @@ -170,6 +174,7 @@ def _copy(cls: type[BU], user: BU) -> BU: self.bot = user.bot self._state = user._state self._public_flags = user._public_flags + self.primary_guild = user.primary_guild return self From 8598e8ae89f5434a18f5556f0d92fe5343352d4b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 03:54:47 +0000 Subject: [PATCH 02/14] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/asset.py | 4 +++- discord/primary_guild.py | 6 ++++-- discord/user.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/discord/asset.py b/discord/asset.py index e66a0d2f1c..a76d85ce02 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -202,7 +202,9 @@ def _from_avatar_decoration( ) @classmethod - def _from_user_primary_guild_tag(cls, state, identity_guild_id: int, badge_id: str) -> Asset: + def _from_user_primary_guild_tag( + cls, state, identity_guild_id: int, badge_id: str + ) -> Asset: """Creates an Asset for a user's primary guild (tag) badge. Parameters diff --git a/discord/primary_guild.py b/discord/primary_guild.py index 172df24481..3defabcaa2 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -56,7 +56,7 @@ def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: def __repr__(self) -> str: return f"" - + @property def badge(self) -> Asset | None: """Returns the badge asset, if available. @@ -68,7 +68,9 @@ def badge(self) -> Asset | None: """ if self._badge is None: return None - return Asset._from_user_primary_guild_tag(self._state, self.identity_guild_id, self._badge) + return Asset._from_user_primary_guild_tag( + self._state, self.identity_guild_id, self._badge + ) __all__ = ("PrimaryGuild",) diff --git a/discord/user.py b/discord/user.py index 148e137757..7167bf78d5 100644 --- a/discord/user.py +++ b/discord/user.py @@ -29,13 +29,13 @@ import discord.abc -from .primary_guild import PrimaryGuild from .asset import Asset from .collectibles import Nameplate from .colour import Colour from .flags import PublicUserFlags from .iterators import EntitlementIterator from .monetization import Entitlement +from .primary_guild import PrimaryGuild from .utils import MISSING, _bytes_to_base64_data, snowflake_time if TYPE_CHECKING: From 27aabd40937a411486852913daadaec9ab02f0cb Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sun, 24 Aug 2025 06:07:47 +0200 Subject: [PATCH 03/14] Instantiate PrimaryGuild object in BaseUser Replaces direct assignment of primary_guild with instantiation of a PrimaryGuild object when data is present, ensuring consistent object structure for user attributes. --- discord/user.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/discord/user.py b/discord/user.py index 7167bf78d5..2151d3b2f9 100644 --- a/discord/user.py +++ b/discord/user.py @@ -154,7 +154,11 @@ def _update(self, data: UserPayload) -> None: self.nameplate = Nameplate(data=nameplate, state=self._state) else: self.nameplate = None - self.primary_guild = data.get("primary_guild", None) + primary_guild = data.get("primary_guild", None) + if primary_guild: + self.primary_guild = PrimaryGuild(data=primary_guild, state=self._state) + else: + self.primary_guild = None self._public_flags = data.get("public_flags", 0) self.bot = data.get("bot", False) self.system = data.get("system", False) From 4fe7d77cc8afef15d622676a75987bb3f9af62f6 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sun, 24 Aug 2025 06:19:28 +0200 Subject: [PATCH 04/14] Check identity_enabled for primary_guild assignment Now assigns primary_guild only if 'identity_enabled' is set in the guild data, preventing assignment when the flag is missing or false. --- discord/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/user.py b/discord/user.py index 2151d3b2f9..f7d49a19d5 100644 --- a/discord/user.py +++ b/discord/user.py @@ -155,7 +155,7 @@ def _update(self, data: UserPayload) -> None: else: self.nameplate = None primary_guild = data.get("primary_guild", None) - if primary_guild: + if primary_guild and primary_guild.get("identity_enabled"): self.primary_guild = PrimaryGuild(data=primary_guild, state=self._state) else: self.primary_guild = None From df73e36997b24050f997a8ba79ca53d0e27f8e02 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sun, 24 Aug 2025 14:44:21 +0200 Subject: [PATCH 05/14] Update discord/primary_guild.py Signed-off-by: Lala Sabathil --- discord/primary_guild.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/primary_guild.py b/discord/primary_guild.py index 3defabcaa2..ed5fedbeec 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -62,9 +62,6 @@ def badge(self) -> Asset | None: """Returns the badge asset, if available. .. versionadded:: 2.7 - - .. note:: - This information is only available via :meth:`Client.fetch_user`. """ if self._badge is None: return None From 1e717ce84d2d19614ca1b896d6f470de50d47428 Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 26 Aug 2025 23:28:16 +0200 Subject: [PATCH 06/14] :memo: Fix docstring indentation Signed-off-by: plun1331 <49261529+plun1331@users.noreply.github.com> --- discord/primary_guild.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/discord/primary_guild.py b/discord/primary_guild.py index ed5fedbeec..3e7da19f59 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -39,12 +39,12 @@ class PrimaryGuild: Attributes ---------- - identity_guild_id: int - The ID of the guild. - identity_enabled: :class:`bool` - Whether the primary guild is enabled. - tag: str - The tag of the primary guild. + identity_guild_id: int + The ID of the guild. + identity_enabled: :class:`bool` + Whether the primary guild is enabled. + tag: str + The tag of the primary guild. """ def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: From 6363e7bc234951057247cdeb454bc3b2465602e1 Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 26 Aug 2025 23:30:21 +0200 Subject: [PATCH 07/14] :adhesive_bandage: Convert to int Signed-off-by: UK <41271523+neloblivion@users.noreply.github.com> --- discord/primary_guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/primary_guild.py b/discord/primary_guild.py index 3e7da19f59..c6fad1ca17 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -48,7 +48,7 @@ class PrimaryGuild: """ def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: - self.identity_guild_id: int = data["identity_guild_id"] + self.identity_guild_id: int = int(data["identity_guild_id"]) self.identity_enabled: bool | None = data.get("identity_enabled", None) self.tag: str = data["tag"] self._badge: str = data["badge"] From 9a7022b17f7f8aa039976c98f2c6a337d925e029 Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 26 Aug 2025 23:32:31 +0200 Subject: [PATCH 08/14] :adhesive_bandage: primary_guild -> primary_guild_payload --- discord/user.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/discord/user.py b/discord/user.py index f7d49a19d5..c8ad8bc017 100644 --- a/discord/user.py +++ b/discord/user.py @@ -154,9 +154,11 @@ def _update(self, data: UserPayload) -> None: self.nameplate = Nameplate(data=nameplate, state=self._state) else: self.nameplate = None - primary_guild = data.get("primary_guild", None) - if primary_guild and primary_guild.get("identity_enabled"): - self.primary_guild = PrimaryGuild(data=primary_guild, state=self._state) + primary_guild_payload = data.get("primary_guild", None) + if primary_guild_payload and primary_guild_payload.get("identity_enabled"): + self.primary_guild = PrimaryGuild( + data=primary_guild_payload, state=self._state + ) else: self.primary_guild = None self._public_flags = data.get("public_flags", 0) From 41aaf40e712d6921ed60c77f113c1a6885f34449 Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 26 Aug 2025 23:33:06 +0200 Subject: [PATCH 09/14] :label: Add typing --- discord/asset.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/asset.py b/discord/asset.py index a76d85ce02..bcdea863a2 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -39,6 +39,8 @@ if TYPE_CHECKING: ValidStaticFormatTypes = Literal["webp", "jpeg", "jpg", "png"] ValidAssetFormatTypes = Literal["webp", "jpeg", "jpg", "png", "gif"] + from .state import ConnectionState + VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"}) VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"} @@ -203,7 +205,7 @@ def _from_avatar_decoration( @classmethod def _from_user_primary_guild_tag( - cls, state, identity_guild_id: int, badge_id: str + cls, state: ConnectionState, identity_guild_id: int, badge_id: str ) -> Asset: """Creates an Asset for a user's primary guild (tag) badge. From e25221e78431f1a69971d7dd1eb49a85f83ee326 Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 26 Aug 2025 23:36:07 +0200 Subject: [PATCH 10/14] :art: move `__all__` at the top --- discord/primary_guild.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/discord/primary_guild.py b/discord/primary_guild.py index c6fad1ca17..8b61fbfa67 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -30,6 +30,8 @@ from .asset import Asset from .types.primary_guild import PrimaryGuild as PrimaryGuildPayload +__all__ = ("PrimaryGuild",) + class PrimaryGuild: """ @@ -68,6 +70,3 @@ def badge(self) -> Asset | None: return Asset._from_user_primary_guild_tag( self._state, self.identity_guild_id, self._badge ) - - -__all__ = ("PrimaryGuild",) From c0806ec67664fd8919342b2308d97d3a737b3ebb Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Thu, 28 Aug 2025 10:37:56 +0200 Subject: [PATCH 11/14] Update discord/primary_guild.py Signed-off-by: Lala Sabathil --- discord/primary_guild.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/primary_guild.py b/discord/primary_guild.py index 8b61fbfa67..554618d427 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -50,10 +50,10 @@ class PrimaryGuild: """ def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: - self.identity_guild_id: int = int(data["identity_guild_id"]) + self.identity_guild_id: int | None = int(data.get("identity_guild_id", 0)) or None self.identity_enabled: bool | None = data.get("identity_enabled", None) - self.tag: str = data["tag"] - self._badge: str = data["badge"] + self.tag: str | None = data.get("tag", None) + self._badge: str | None = data.get("badge", None) self._state: "ConnectionState" = state def __repr__(self) -> str: From d4e6f274f30c4e8918589afa9fe9d82ee82ae7d1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 08:42:22 +0000 Subject: [PATCH 12/14] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/primary_guild.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/primary_guild.py b/discord/primary_guild.py index 554618d427..de525afb5d 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -50,7 +50,9 @@ class PrimaryGuild: """ def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: - self.identity_guild_id: int | None = int(data.get("identity_guild_id", 0)) or None + self.identity_guild_id: int | None = ( + int(data.get("identity_guild_id", 0)) or None + ) self.identity_enabled: bool | None = data.get("identity_enabled", None) self.tag: str | None = data.get("tag", None) self._badge: str | None = data.get("badge", None) From dbe63e2da570a1356d70b2538755251b849c40cc Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Thu, 28 Aug 2025 10:50:17 +0200 Subject: [PATCH 13/14] Apply suggestion from @Lulalaby Signed-off-by: Lala Sabathil --- discord/primary_guild.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/discord/primary_guild.py b/discord/primary_guild.py index de525afb5d..7499978666 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -50,9 +50,7 @@ class PrimaryGuild: """ def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: - self.identity_guild_id: int | None = ( - int(data.get("identity_guild_id", 0)) or None - ) + self.identity_guild_id: int | None = int(data.get("identity_guild_id") or 0) or None self.identity_enabled: bool | None = data.get("identity_enabled", None) self.tag: str | None = data.get("tag", None) self._badge: str | None = data.get("badge", None) From ac2ef05d22db28f6b49f3c22ec6d272417103af8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 08:50:43 +0000 Subject: [PATCH 14/14] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/primary_guild.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/primary_guild.py b/discord/primary_guild.py index 7499978666..13e6ae035f 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -50,7 +50,9 @@ class PrimaryGuild: """ def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: - self.identity_guild_id: int | None = int(data.get("identity_guild_id") or 0) or None + self.identity_guild_id: int | None = ( + int(data.get("identity_guild_id") or 0) or None + ) self.identity_enabled: bool | None = data.get("identity_enabled", None) self.tag: str | None = data.get("tag", None) self._badge: str | None = data.get("badge", None)