Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
236e8ec
feat: Add SharedClientTheme support for customizable message appearance
UnBonWhisky Apr 20, 2026
9116f68
fix: Remove redundant validation checks for shared_client_theme
UnBonWhisky Apr 20, 2026
7c2ae1e
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 20, 2026
03e795b
fix: Update versionadded for SharedClientTheme to 2.9 across multiple…
UnBonWhisky Apr 20, 2026
2ac124b
fix: Add SharedClientTheme in changelog
UnBonWhisky Apr 20, 2026
e940162
fix: Update the SharedClientTheme constructor to mark the required ar…
UnBonWhisky Apr 21, 2026
d41f8f6
Update discord/shared_client_theme.py
UnBonWhisky May 10, 2026
d4c1a55
Update CHANGELOG.md
UnBonWhisky May 10, 2026
73c3ce1
Update discord/abc.py
UnBonWhisky May 10, 2026
6234b77
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 10, 2026
d96ea81
fix: applied the comments and used colour as the main and color as an…
May 17, 2026
2ad9447
docs: moved SharedClientTheme from data_class to models
May 17, 2026
b8a1994
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 17, 2026
9e84f08
Merge branch 'master' into master
Paillat-dev May 22, 2026
4401b08
fix: refactor SharedClientTheme initialization and color handling
May 23, 2026
c99567d
fix: update from_dict method to use Self type hint
May 23, 2026
23bd2cf
fix: add parentheses to SharedClientTheme autoclass declaration
May 23, 2026
7d4d65d
fix: refactor SharedClientTheme to use dataclass and update type hints
May 23, 2026
5a1f0c1
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 23, 2026
aa00312
fix: update SharedClientTheme to use field for colours with default f…
May 23, 2026
12adfcb
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 23, 2026
b827ed1
fix: update SharedClientTheme to use field for colours and adjust dat…
May 23, 2026
a53d601
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 23, 2026
4e8926c
fix: remove __repr__ method from SharedClientTheme class
May 23, 2026
28c746f
Update discord/message.py
UnBonWhisky May 23, 2026
cfdcb80
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 23, 2026
383b28e
Update CHANGELOG.md
UnBonWhisky Jun 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ released.

### Added

- Added support for sending and receiving client themes.
([#3209](https://github.com/Pycord-Development/pycord/pull/3209))

### Changed

### Fixed
Expand Down
1 change: 1 addition & 0 deletions discord/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
from .role import *
from .scheduled_events import *
from .shard import *
from .shared_client_theme import *
from .soundboard import *
from .stage_instance import *
from .sticker import *
Expand Down
16 changes: 16 additions & 0 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
from .permissions import PermissionOverwrite, Permissions
from .role import Role
from .scheduled_events import ScheduledEvent
from .shared_client_theme import SharedClientThemeBaseType
from .sticker import GuildSticker, StickerItem
from .utils import warn_deprecated

Expand Down Expand Up @@ -89,6 +90,7 @@
from .member import Member
from .message import Message, MessageReference, PartialMessage
from .poll import Poll
from .shared_client_theme import SharedClientTheme
from .state import ConnectionState
from .threads import Thread
from .types.channel import Channel as ChannelPayload
Expand Down Expand Up @@ -1388,6 +1390,7 @@ async def send(
suppress: bool = ...,
suppress_embeds: bool = ...,
silent: bool = ...,
shared_client_theme: SharedClientTheme = ...,
) -> Message: ...

@overload
Expand All @@ -1410,6 +1413,7 @@ async def send(
suppress: bool = ...,
suppress_embeds: bool = ...,
silent: bool = ...,
shared_client_theme: SharedClientTheme = ...,
) -> Message: ...

@overload
Expand All @@ -1432,6 +1436,7 @@ async def send(
suppress: bool = ...,
suppress_embeds: bool = ...,
silent: bool = ...,
shared_client_theme: SharedClientTheme = ...,
) -> Message: ...

@overload
Expand All @@ -1454,6 +1459,7 @@ async def send(
suppress: bool = ...,
suppress_embeds: bool = ...,
silent: bool = ...,
shared_client_theme: SharedClientTheme = ...,
) -> Message: ...

async def send(
Expand All @@ -1477,6 +1483,7 @@ async def send(
suppress=None,
suppress_embeds=None,
silent=None,
shared_client_theme=None,
):
"""|coro|

Expand Down Expand Up @@ -1568,6 +1575,10 @@ async def send(
The poll to send.

.. versionadded:: 2.6
shared_client_theme: :class:`SharedClientTheme`
The shared client theme to send.

.. versionadded:: 2.9

Returns
-------
Expand All @@ -1592,6 +1603,9 @@ async def send(
state = self._state
content = str(content) if content is not None else None

if shared_client_theme is not None:
shared_client_theme = shared_client_theme.to_dict()

if embed is not None and embeds is not None:
raise InvalidArgument(
"cannot pass both embed and embeds parameter to send()"
Expand Down Expand Up @@ -1706,6 +1720,7 @@ async def send(
components=components,
flags=flags.value,
poll=poll,
shared_client_theme=shared_client_theme,
)
finally:
for f in files:
Expand All @@ -1725,6 +1740,7 @@ async def send(
components=components,
flags=flags.value,
poll=poll,
shared_client_theme=shared_client_theme,
)

ret = state.create_message(channel=channel, data=data)
Expand Down
9 changes: 9 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"SelectDefaultValueType",
"ApplicationEventWebhookStatus",
"InviteTargetUsersJobStatusCode",
"SharedClientThemeBaseType",
)


Expand Down Expand Up @@ -1212,6 +1213,14 @@ class InviteTargetUsersJobStatusCode(Enum):
failed = 3


class SharedClientThemeBaseType(Enum):
unset = 0
dark = 1
light = 2
darker = 3
midnight = 4


T = TypeVar("T")


Expand Down
11 changes: 11 additions & 0 deletions discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
poll,
role,
scheduled_events,
shared_client_theme,
sticker,
template,
threads,
Expand Down Expand Up @@ -507,6 +508,7 @@ def send_message(
components: list[components.Component] | None = None,
flags: int | None = None,
poll: poll.Poll | None = None,
shared_client_theme: shared_client_theme.SharedClientTheme | None = None,
) -> Response[message.Message]:
r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id)
payload = {}
Expand Down Expand Up @@ -547,6 +549,9 @@ def send_message(
if poll:
payload["poll"] = poll

if shared_client_theme:
payload["shared_client_theme"] = shared_client_theme

return self.request(r, json=payload)

def send_typing(self, channel_id: Snowflake) -> Response[None]:
Expand All @@ -571,6 +576,7 @@ def send_multipart_helper(
components: list[components.Component] | None = None,
flags: int | None = None,
poll: poll.Poll | None = None,
shared_client_theme: shared_client_theme.SharedClientTheme | None = None,
) -> Response[message.Message]:
form = []

Expand Down Expand Up @@ -598,6 +604,9 @@ def send_multipart_helper(
if poll:
payload["poll"] = poll

if shared_client_theme:
payload["shared_client_theme"] = shared_client_theme

attachments = []
form.append({"name": "payload_json"})
for index, file in enumerate(files):
Expand Down Expand Up @@ -641,6 +650,7 @@ def send_files(
components: list[components.Component] | None = None,
flags: int | None = None,
poll: poll.Poll | None = None,
shared_client_theme: shared_client_theme.SharedClientTheme | None = None,
) -> Response[message.Message]:
r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id)
return self.send_multipart_helper(
Expand All @@ -658,6 +668,7 @@ def send_files(
components=components,
flags=flags,
poll=poll,
shared_client_theme=shared_client_theme,
)

def edit_multipart_helper(
Expand Down
17 changes: 17 additions & 0 deletions discord/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from .partial_emoji import PartialEmoji
from .poll import Poll
from .reaction import Reaction
from .shared_client_theme import SharedClientTheme
from .sticker import StickerItem
from .threads import Thread
from .utils import MISSING, escape_mentions, find, warn_deprecated
Expand Down Expand Up @@ -90,6 +91,7 @@
from .types.message import MessageSnapshot as MessageSnapshotPayload
from .types.message import Reaction as ReactionPayload
from .types.poll import Poll as PollPayload
from .types.shared_client_theme import SharedClientTheme as SharedClientThemePayload
from .types.snowflake import SnowflakeList
from .types.threads import ThreadArchiveDuration
from .types.user import User as UserPayload
Expand Down Expand Up @@ -1039,6 +1041,10 @@ class Message(Hashable):
The poll associated with this message, if applicable.

.. versionadded:: 2.6
shared_client_theme: Optional[:class:`SharedClientTheme`]
The shared client theme transmitted via this message, if applicable.

.. versionadded:: 2.9
call: Optional[:class:`MessageCall`]
The call information associated with this message, if applicable.

Expand Down Expand Up @@ -1087,6 +1093,7 @@ class Message(Hashable):
"_poll",
"call",
"snapshots",
"shared_client_theme",
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -1217,6 +1224,13 @@ def __init__(
except KeyError:
self.call = None

if shared_client_theme := data.get("shared_client_theme"):
self.shared_client_theme: SharedClientTheme | None = (
SharedClientTheme.from_dict(shared_client_theme)
)
else:
self.shared_client_theme = None

for handler in ("author", "member", "mentions", "mention_roles"):
try:
getattr(self, f"_handle_{handler}")(data[handler])
Expand Down Expand Up @@ -1328,6 +1342,9 @@ def _handle_poll(self, value: PollPayload) -> None:
self._poll = Poll.from_dict(value, self)
self._state.store_poll(self._poll, self.id)

def _handle_shared_client_theme(self, value: SharedClientThemePayload) -> None:
self.shared_client_theme = SharedClientTheme.from_dict(value)

def _handle_author(self, author: UserPayload) -> None:
self.author = self._state.store_user(author)
if isinstance(self.guild, Guild):
Expand Down
131 changes: 131 additions & 0 deletions discord/shared_client_theme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
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 dataclasses import dataclass
from typing import TYPE_CHECKING, Sequence

from typing_extensions import Self

from .colour import Colour
from .enums import SharedClientThemeBaseType, try_enum
from .utils import MISSING

__all__ = ("SharedClientTheme",)


if TYPE_CHECKING:
from .types.shared_client_theme import SharedClientTheme as SharedClientThemePayload


@dataclass(init=False)
class SharedClientTheme:
"""Represents a shared client theme that can be sent in a message.

A shared client theme lets users transmit a customized Discord client
appearance (colors, gradient, and base mode) through a message.

.. versionadded:: 2.9

Attributes
----------
colours: List[:class:`Colour`]
The colours of the theme. A maximum of 5 colours.
colors: List[:class:`Colour`]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be documented in the property method not here since it's not an attribute technically.

Alias for :attr:`colours`.
gradient_angle: :class:`int`
The direction of the theme's colors, in degrees. Must be between ``0`` and ``360``.
base_mix: :class:`int`
The intensity of the theme's colors. Must be between ``0`` and ``100``.
base_theme: Optional[:class:`SharedClientThemeBaseType`]
The mode of the theme. Defaults to :attr:`SharedClientThemeBaseType.unset`,
which Discord treats as :attr:`SharedClientThemeBaseType.dark`.
"""

gradient_angle: int = 0
base_mix: int = 0
colours: list[Colour]
base_theme: SharedClientThemeBaseType | None = SharedClientThemeBaseType.unset

def __init__(
self,
gradient_angle: int = 0,
base_mix: int = 0,
colors: Sequence[Colour] = MISSING,
colours: Sequence[Colour] = MISSING,
*,
base_theme: SharedClientThemeBaseType = SharedClientThemeBaseType.unset,
) -> None:
colours = colours if colours is not MISSING else colors

if len(colours or []) > 5:
raise ValueError("colors or colours must contain at most 5 colours")
if colours is MISSING:
raise TypeError("colors or colours must be provided")

if not 0 <= gradient_angle <= 360:
raise ValueError("gradient_angle must be between 0 and 360")

if not 0 <= base_mix <= 100:
raise ValueError("base_mix must be between 0 and 100")

if base_theme is not None and not isinstance(
base_theme, SharedClientThemeBaseType
):
raise TypeError("base_theme must be a SharedClientThemeBaseType or None")

self.gradient_angle = gradient_angle
self.base_mix = base_mix
self.colours = list(colours)
self.base_theme = base_theme

@property
def colors(self) -> list[Colour]:
return self.colours

def to_dict(self) -> SharedClientThemePayload:
payload: SharedClientThemePayload = {
"colors": [f"{c.value:0>6x}" for c in self.colours],
"gradient_angle": self.gradient_angle,
"base_mix": self.base_mix,
}
if self.base_theme is not None:
payload["base_theme"] = self.base_theme.value
return payload

@classmethod
def from_dict(cls, data: SharedClientThemePayload) -> Self:
base_theme_value = data.get("base_theme")
colours = [Colour(int(c, 16)) for c in data.get("colors", [])]
return cls(
colours=colours,
gradient_angle=data["gradient_angle"],
base_mix=data["base_mix"],
base_theme=(
try_enum(SharedClientThemeBaseType, base_theme_value)
if base_theme_value is not None
else None
),
)
Loading
Loading