diff --git a/discord/enums.py b/discord/enums.py index 025b54cb4..11c631c37 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -87,6 +87,8 @@ __all__ = ( 'MediaItemLoadingState', 'CollectibleType', 'NameplatePalette', + 'DisplayNameFont', + 'DisplayNameEffect', ) @@ -1006,6 +1008,30 @@ class NameplatePalette(Enum): white = 'white' +class DisplayNameFont(Enum): + default = 11 + bangers = 1 + bio_rhyme = 2 + cherry_bomb = 3 + chicle = 4 + compagnon = 5 + museo_moderno = 6 + neo_castel = 7 + pixelify = 8 + ribes = 9 + sinistre = 10 + zilla_slab = 12 + + +class DisplayNameEffect(Enum): + solid = 1 + gradient = 2 + neon = 3 + toon = 4 + pop = 5 + glow = 6 + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/member.py b/discord/member.py index 8f7342877..0095aa188 100644 --- a/discord/member.py +++ b/discord/member.py @@ -36,12 +36,13 @@ from .asset import Asset from .utils import MISSING from .user import BaseUser, ClientUser, User, _UserTag from .permissions import Permissions -from .enums import Status +from .enums import DisplayNameEffect, DisplayNameFont, Status from .errors import ClientException from .colour import Colour from .object import Object from .flags import MemberFlags from .presences import ClientStatus +from .user import DisplayNameStyle __all__ = ( 'VoiceState', @@ -64,7 +65,7 @@ if TYPE_CHECKING: UserWithMember as UserWithMemberPayload, ) from .types.gateway import GuildMemberUpdateEvent - from .types.user import User as UserPayload, AvatarDecorationData + from .types.user import User as UserPayload, AvatarDecorationData, DisplayNameStyle as DisplayNameStylePayload from .abc import Snowflake from .state import ConnectionState from .message import Message @@ -289,6 +290,7 @@ class Member(discord.abc.Messageable, _UserTag): '_banner', '_flags', '_avatar_decoration_data', + '_display_name_style', ) if TYPE_CHECKING: @@ -329,6 +331,7 @@ class Member(discord.abc.Messageable, _UserTag): self._permissions: Optional[int] self._flags: int = data['flags'] self._avatar_decoration_data: Optional[AvatarDecorationData] = data.get('avatar_decoration_data') + self._display_name_style: Optional[DisplayNameStylePayload] = data.get('display_name_style') try: self._permissions = int(data['permissions']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: @@ -408,6 +411,7 @@ class Member(discord.abc.Messageable, _UserTag): self._avatar = member._avatar self._banner = member._banner self._avatar_decoration_data = member._avatar_decoration_data + self._display_name_style = member._display_name_style # Reference will not be copied unless necessary by PRESENCE_UPDATE # See below @@ -438,6 +442,7 @@ class Member(discord.abc.Messageable, _UserTag): self._banner = data.get('banner') self._flags = data.get('flags', 0) self._avatar_decoration_data = data.get('avatar_decoration_data') + self._display_name_style = data.get('display_name_style') def _presence_update(self, raw: RawPresenceUpdateEvent, user: UserPayload) -> Optional[Tuple[User, User]]: self.activities = raw.activities @@ -456,10 +461,12 @@ class Member(discord.abc.Messageable, _UserTag): u._public_flags, u._avatar_decoration_data['sku_id'] if u._avatar_decoration_data is not None else None, u._primary_guild, + u._display_name_style, ) decoration_payload = user.get('avatar_decoration_data') primary_guild_payload = user.get('primary_guild', None) + display_name_style_payload = user.get('display_name_style', None) # These keys seem to always be available modified = ( user['username'], @@ -469,6 +476,7 @@ class Member(discord.abc.Messageable, _UserTag): user.get('public_flags', 0), decoration_payload['sku_id'] if decoration_payload is not None else None, primary_guild_payload, + display_name_style_payload, ) if original != modified: to_return = User._copy(self._user) @@ -480,6 +488,7 @@ class Member(discord.abc.Messageable, _UserTag): u._public_flags, u._avatar_decoration_data, u._primary_guild, + u._display_name_style, ) = ( user['username'], user['discriminator'], @@ -488,6 +497,7 @@ class Member(discord.abc.Messageable, _UserTag): user.get('public_flags', 0), decoration_payload, primary_guild_payload, + display_name_style_payload, ) # Signal to dispatch on_user_update return to_return, u @@ -659,6 +669,35 @@ class Member(discord.abc.Messageable, _UserTag): return None return Asset._from_guild_banner(self._state, self.guild.id, self.id, self._banner) + @property + def display_name_style(self) -> Optional[DisplayNameStyle]: + """Optional[:class:`DisplayNameStyle`]: Returns the member's guild display name style if they have one, + otherwise their global display name style if they have one, otherwise ``None``. + + .. versionadded:: 2.8 + """ + return self.guild_display_name_style or self.global_display_name_style + + @property + def guild_display_name_style(self) -> Optional[DisplayNameStyle]: + """Optional[:class:`DisplayNameStyle`]: Returns the member's guild specific display name style. + if the member has no guild display name style set then ``None`` is returned. + + .. versionadded:: 2.8 + """ + if self._display_name_style is None: + return None + return DisplayNameStyle(data=self._display_name_style) + + @property + def global_display_name_style(self) -> Optional[DisplayNameStyle]: + """Optional[:class:`DisplayNameStyle`]: Returns the user's global display name style. + if the user has no global display name style set then ``None`` is returned. + + .. versionadded:: 2.8 + """ + return self._user.display_name_style + @property def activity(self) -> Optional[ActivityTypes]: """Optional[Union[:class:`BaseActivity`, :class:`Spotify`]]: Returns the primary @@ -817,6 +856,9 @@ class Member(discord.abc.Messageable, _UserTag): avatar: Optional[bytes] = MISSING, banner: Optional[bytes] = MISSING, bio: Optional[str] = MISSING, + display_name_font: Optional[DisplayNameFont] = MISSING, + display_name_effect: Optional[DisplayNameEffect] = MISSING, + display_name_colors: Optional[List[Union[Colour, int]]] = MISSING, reason: Optional[str] = None, ) -> Optional[Member]: """|coro| @@ -902,7 +944,22 @@ class Member(discord.abc.Messageable, _UserTag): This can only be set when editing the bot's own member. .. versionadded:: 2.7 + display_name_font: Optional[:class:`DisplayNameFont`] + The new display name font for the member. Use ``None`` to remove the display name font. + This can only be set when editing the bot's own member. + + .. versionadded:: 2.8 + display_name_effect: Optional[:class:`DisplayNameEffect`] + The new display name effect for the member. Use ``None`` to remove the display name effect. + This can only be set when editing the bot's own member. + + .. versionadded:: 2.8 + display_name_colors: Optional[List[Union[:class:`Colour`, :class:`int`]]] + A list of up to 2 colors that will be used for the member's display name + gradient. This can only be set when editing the bot's own member. + Use ``None`` to remove the display name colors. + .. versionadded:: 2.8 reason: Optional[:class:`str`] The reason for editing this member. Shows up on the audit log. @@ -952,8 +1009,21 @@ class Member(discord.abc.Messageable, _UserTag): if bio is not MISSING: self_payload['bio'] = bio or '' + if display_name_font is not MISSING: + self_payload['display_name_font_id'] = display_name_font.value if display_name_font else None + + if display_name_effect is not MISSING: + self_payload['display_name_effect_id'] = display_name_effect.value if display_name_effect else None + + if display_name_colors is not MISSING: + self_payload['display_name_colors'] = [ + c.value if isinstance(c, Colour) else c for c in display_name_colors or [] + ] + if not me and self_payload: - raise ValueError("Editing the bio, avatar or banner is only for the bot's own member.") + only_fields = {'avatar', 'banner', 'bio', 'display_name_font', 'display_name_effect', 'display_name_colors'} + current_fields = utils._human_join(list(only_fields.intersection(set(self_payload.keys()))), final='and') + raise ValueError(f"Editing {current_fields} can only happen when editing the bot's own member.") if deafen is not MISSING: payload['deaf'] = deafen diff --git a/discord/types/member.py b/discord/types/member.py index 576ef421d..00e3ae383 100644 --- a/discord/types/member.py +++ b/discord/types/member.py @@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE. from typing import Optional, TypedDict from .snowflake import SnowflakeList -from .user import User, AvatarDecorationData +from .user import User, AvatarDecorationData, DisplayNameStyle from typing_extensions import NotRequired @@ -50,6 +50,7 @@ class Member(PartialMember, total=False): communication_disabled_until: str banner: NotRequired[Optional[str]] avatar_decoration_data: NotRequired[AvatarDecorationData] + display_name_style: NotRequired[DisplayNameStyle] class _OptionalMemberWithUser(PartialMember, total=False): @@ -60,6 +61,7 @@ class _OptionalMemberWithUser(PartialMember, total=False): permissions: str communication_disabled_until: str avatar_decoration_data: NotRequired[AvatarDecorationData] + display_name_style: NotRequired[DisplayNameStyle] class MemberWithUser(_OptionalMemberWithUser): diff --git a/discord/types/user.py b/discord/types/user.py index 639384a56..d1c218f4e 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -23,12 +23,14 @@ DEALINGS IN THE SOFTWARE. """ from .snowflake import Snowflake -from typing import Literal, Optional, TypedDict +from typing import Literal, Optional, TypedDict, List from typing_extensions import NotRequired PremiumType = Literal[0, 1, 2, 3] NameplatePallete = Literal['crimson', 'berry', 'sky', 'teal', 'forest', 'bubble_gum', 'violet', 'cobalt', 'clover'] +DisplayNameFont = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] +DisplayNameEffect = Literal[1, 2, 3, 4, 5] class _UserSKU(TypedDict): @@ -70,6 +72,12 @@ class PartialUser(TypedDict): collectibles: NotRequired[UserCollectibles] +class DisplayNameStyle(TypedDict): + font_id: DisplayNameFont + effect_id: DisplayNameEffect + colors: List[int] + + class User(PartialUser, total=False): bot: bool system: bool @@ -80,3 +88,4 @@ class User(PartialUser, total=False): flags: int premium_type: PremiumType public_flags: int + display_name_styles: DisplayNameStyle diff --git a/discord/user.py b/discord/user.py index 32edb1dc7..d5d97a62e 100644 --- a/discord/user.py +++ b/discord/user.py @@ -29,7 +29,7 @@ from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union import discord.abc from .asset import Asset from .colour import Colour -from .enums import DefaultAvatar +from .enums import DefaultAvatar, DisplayNameEffect, DisplayNameFont, try_enum from .flags import PublicUserFlags from .utils import snowflake_time, _bytes_to_base64_data, MISSING, _get_as_snowflake from .primary_guild import PrimaryGuild @@ -51,15 +51,39 @@ if TYPE_CHECKING: AvatarDecorationData, PrimaryGuild as PrimaryGuildPayload, UserCollectibles as UserCollectiblesPayload, + DisplayNameStyle as DisplayNameStylePayload, ) __all__ = ( 'User', 'ClientUser', + 'DisplayNameStyle', ) +class DisplayNameStyle: + """Represents a user's display name style. + + Attributes + ----------- + font: :class:`DisplayNameFont` + The font. + effect: :class:`DisplayNameEffect` + The applied effect. + colors: List[:class:`Colour`] + The colors used in the effect. Max of 2. + """ + + def __init__(self, *, data: DisplayNameStylePayload) -> None: + self.font: DisplayNameFont = try_enum(DisplayNameFont, data['font_id']) + self.effect: DisplayNameEffect = try_enum(DisplayNameEffect, data['effect_id']) + self.colors: List[discord.Colour] = [discord.Colour(color) for color in data.get('colors', [])] + + def __repr__(self) -> str: + return f'' + + class _UserTag: __slots__ = () id: int @@ -81,6 +105,7 @@ class BaseUser(_UserTag): '_avatar_decoration_data', '_primary_guild', '_collectibles', + '_display_name_style', ) if TYPE_CHECKING: @@ -98,6 +123,7 @@ class BaseUser(_UserTag): _avatar_decoration_data: Optional[AvatarDecorationData] _primary_guild: Optional[PrimaryGuildPayload] _collectibles: Optional[UserCollectiblesPayload] + _display_name_style: Optional[DisplayNameStylePayload] def __init__(self, *, state: ConnectionState, data: Union[UserPayload, PartialUserPayload]) -> None: self._state = state @@ -137,6 +163,7 @@ class BaseUser(_UserTag): self._avatar_decoration_data = data.get('avatar_decoration_data') self._primary_guild = data.get('primary_guild', None) self._collectibles = data.get('collectibles', None) + self._display_name_style = data.get('display_name_styles', None) or None @classmethod def _copy(cls, user: Self) -> Self: @@ -155,6 +182,7 @@ class BaseUser(_UserTag): self._avatar_decoration_data = user._avatar_decoration_data self._primary_guild = user._primary_guild self._collectibles = user._collectibles + self._display_name_style = user._display_name_style return self @@ -340,6 +368,16 @@ class BaseUser(_UserTag): return [] return [Collectible(state=self._state, type=key, data=value) for key, value in self._collectibles.items() if value] # type: ignore + @property + def display_name_style(self) -> Optional[DisplayNameStyle]: + """Optional[:class:`DisplayNameStyle`]: Returns the user's display name style. + + .. versionadded:: 2.8 + """ + if self._display_name_style is None: + return None + return DisplayNameStyle(data=self._display_name_style) + def mentioned_in(self, message: Message) -> bool: """Checks if the user is mentioned in the specified message. diff --git a/docs/api.rst b/docs/api.rst index 5ed9ffb39..7f943f719 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4154,6 +4154,90 @@ of :class:`enum.Enum`. The collectible nameplate palette is white. +.. class:: DisplayNameFont + + Represents the available fonts for a user display name style. + + .. versionadded:: 2.8 + + .. attribute:: default + + The default font is used. + + .. attribute:: bangers + + The 'Bangers' font is used. + + .. attribute:: bio_rhyme + + The 'BioRhyme' font is used. + + .. attribute:: cherry_bomb + + The 'Cherry Bomb' font is used. + + .. attribute:: chicle + + The 'Chicle' font is used. + + .. attribute:: compagnon + + The 'Compagnon' font is used. + + .. attribute:: museo_moderno + + The 'Museo Moderno' font is used. + + .. attribute:: neo_castel + + The 'Neo Castel' font is used. + + .. attribute:: pixelify + + The 'Pixelify' font is used. + + .. attribute:: ribes + + The 'Ribes' font is used. + + .. attribute:: sinistre + + The 'Sinistre' font is used. + + .. attribute:: zilla_slab + + The 'Zilla Slab' font is used. + +.. class:: DisplayNameEffect + + Represents the available effects for a user display name style. + + .. versionadded:: 2.8 + + .. attribute:: solid + + The first color provided is used. + + .. attribute:: gradient + + There is a two colour gradient using both colors provided. + + .. attribute:: neon + + There is a neon glow around the name. + + .. attribute:: toon + + There is a subtle vertical gradient and stroke around the name. + + .. attribute:: pop + + A coloured dropshadow is shown. + .. attribute:: glow + + An alternative for the gradient style is used. + + .. _discord-api-audit-logs: Audit Log Data @@ -5859,6 +5943,14 @@ Collectible .. autoclass:: Collectible() :members: +DisplayNameStyle +~~~~~~~~~~~~~~~~~ + +.. attributetable:: DisplayNameStyle + +.. autoclass:: DisplayNameStyle() + :members: + CallMessage ~~~~~~~~~~~~~~~~~~~