diff --git a/changelog/1184.feature.rst b/changelog/1184.feature.rst new file mode 100644 index 0000000000..32aae25dc1 --- /dev/null +++ b/changelog/1184.feature.rst @@ -0,0 +1 @@ +Add the possibility to pass :class:`disnake.File` objects to :meth:`Embed.set_author` and :meth:`~Embed.set_footer`. diff --git a/changelog/1203.feature.rst b/changelog/1203.feature.rst new file mode 100644 index 0000000000..d4412fdf31 --- /dev/null +++ b/changelog/1203.feature.rst @@ -0,0 +1 @@ +Implement new :attr:`.Member.guild_banner` property. diff --git a/disnake/asset.py b/disnake/asset.py index edb0d1c7a6..cf3cf5ac66 100644 --- a/disnake/asset.py +++ b/disnake/asset.py @@ -237,6 +237,19 @@ def _from_guild_avatar( animated=animated, ) + @classmethod + def _from_guild_banner( + cls, state: AnyState, guild_id: int, member_id: int, banner: str + ) -> Self: + animated = banner.startswith("a_") + format = "gif" if animated else "png" + return cls( + state, + url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=1024", + key=banner, + animated=animated, + ) + @classmethod def _from_icon(cls, state: AnyState, object_id: int, icon_hash: str, path: str) -> Self: return cls( diff --git a/disnake/client.py b/disnake/client.py index 80b3d67c65..2c4e2c7da4 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -1272,7 +1272,12 @@ def run(self, *args: Any, **kwargs: Any) -> None: This function must be the last function to call due to the fact that it is blocking. That means that registration of events or anything being - called after this function call will not execute until it returns. + called after this function call will not execute until it returns + + Parameters + ---------- + token: :class:`str` + The discord token of the bot that is being ran. """ loop = self.loop diff --git a/disnake/embeds.py b/disnake/embeds.py index 1866d8d7eb..abbbff53f2 100644 --- a/disnake/embeds.py +++ b/disnake/embeds.py @@ -106,7 +106,7 @@ class _EmbedAuthorProxy(Sized, Protocol): icon_url: Optional[str] proxy_icon_url: Optional[str] - _FileKey = Literal["image", "thumbnail"] + _FileKey = Literal["image", "thumbnail", "footer", "author"] class Embed: @@ -385,12 +385,32 @@ def footer(self) -> _EmbedFooterProxy: """ return cast("_EmbedFooterProxy", EmbedProxy(self._footer)) - def set_footer(self, *, text: Any, icon_url: Optional[Any] = None) -> Self: + @overload + def set_footer(self, *, text: Any, icon_url: Optional[Any] = ...) -> Self: + ... + + @overload + def set_footer(self, *, text: Any, icon_file: File = ...) -> Self: + ... + + def set_footer( + self, *, text: Any, icon_url: Optional[Any] = MISSING, icon_file: File = MISSING + ) -> Self: """Sets the footer for the embed content. This function returns the class instance to allow for fluent-style chaining. + At most one of ``icon_url`` or ``icon_file`` may be passed. + + .. warning:: + Passing a :class:`disnake.File` object will make the embed not + reusable. + + .. warning:: + If used with the other ``set_*`` methods, you must ensure + that the :attr:`.File.filename` is unique to avoid duplication. + Parameters ---------- text: :class:`str` @@ -401,13 +421,18 @@ def set_footer(self, *, text: Any, icon_url: Optional[Any] = None) -> Self: icon_url: Optional[:class:`str`] The URL of the footer icon. Only HTTP(S) is supported. + icon_file: :class:`File` + The file to use as the footer icon. + + .. versionadded:: 2.10 """ self._footer = { "text": str(text), } - if icon_url is not None: - self._footer["icon_url"] = str(icon_url) + result = self._handle_resource(icon_url, icon_file, key="footer", required=False) + if result is not None: + self._footer["icon_url"] = result return self @@ -457,6 +482,10 @@ def set_image(self, url: Optional[Any] = MISSING, *, file: File = MISSING) -> Se Passing a :class:`disnake.File` object will make the embed not reusable. + .. warning:: + If used with the other ``set_*`` methods, you must ensure + that the :attr:`.File.filename` is unique to avoid duplication. + .. versionchanged:: 1.4 Passing ``None`` removes the image. @@ -508,6 +537,10 @@ def set_thumbnail(self, url: Optional[Any] = MISSING, *, file: File = MISSING) - Passing a :class:`disnake.File` object will make the embed not reusable. + .. warning:: + If used with the other ``set_*`` methods, you must ensure + that the :attr:`.File.filename` is unique to avoid duplication. + .. versionchanged:: 1.4 Passing ``None`` removes the thumbnail. @@ -559,18 +592,39 @@ def author(self) -> _EmbedAuthorProxy: """ return cast("_EmbedAuthorProxy", EmbedProxy(self._author)) + @overload + def set_author( + self, *, name: Any, url: Optional[Any] = ..., icon_url: Optional[Any] = ... + ) -> Self: + ... + + @overload + def set_author(self, *, name: Any, url: Optional[Any] = ..., icon_file: File = ...) -> Self: + ... + def set_author( self, *, name: Any, url: Optional[Any] = None, - icon_url: Optional[Any] = None, + icon_url: Optional[Any] = MISSING, + icon_file: File = MISSING, ) -> Self: """Sets the author for the embed content. This function returns the class instance to allow for fluent-style chaining. + At most one of ``icon_url`` or ``icon_file`` may be passed. + + .. warning:: + Passing a :class:`disnake.File` object will make the embed not + reusable. + + .. warning:: + If used with the other ``set_*`` methods, you must ensure + that the :attr:`.File.filename` is unique to avoid duplication. + Parameters ---------- name: :class:`str` @@ -579,6 +633,10 @@ def set_author( The URL for the author. icon_url: Optional[:class:`str`] The URL of the author icon. Only HTTP(S) is supported. + icon_file: :class:`File` + The file to use as the author icon. + + .. versionadded:: 2.10 """ self._author = { "name": str(name), @@ -587,8 +645,9 @@ def set_author( if url is not None: self._author["url"] = str(url) - if icon_url is not None: - self._author["icon_url"] = str(icon_url) + result = self._handle_resource(icon_url, icon_file, key="author", required=False) + if result is not None: + self._author["icon_url"] = result return self @@ -821,9 +880,15 @@ def get_default_colour(cls) -> Optional[Colour]: get_default_color = get_default_colour - def _handle_resource(self, url: Optional[Any], file: File, *, key: _FileKey) -> Optional[str]: - if not (url is MISSING) ^ (file is MISSING): - raise TypeError("Exactly one of url or file must be provided") + def _handle_resource( + self, url: Optional[Any], file: Optional[File], *, key: _FileKey, required: bool = True + ) -> Optional[str]: + if required: + if not (url is MISSING) ^ (file is MISSING): + raise TypeError("Exactly one of url or file must be provided") + else: + if url is not MISSING and file is not MISSING: + raise TypeError("At most one of url or file may be provided, not both.") if file: if file.filename is None: @@ -832,7 +897,7 @@ def _handle_resource(self, url: Optional[Any], file: File, *, key: _FileKey) -> return f"attachment://{file.filename}" else: self._files.pop(key, None) - return str(url) if url is not None else None + return str(url) if url else None def check_limits(self) -> None: """Checks if this embed fits within the limits dictated by Discord. diff --git a/disnake/member.py b/disnake/member.py index 7d7bff178e..c8484bb9e7 100644 --- a/disnake/member.py +++ b/disnake/member.py @@ -272,6 +272,7 @@ class Member(disnake.abc.Messageable, _UserTag): "_user", "_state", "_avatar", + "_banner", "_communication_disabled_until", "_flags", "_avatar_decoration_data", @@ -340,6 +341,7 @@ def __init__( self.nick: Optional[str] = data.get("nick") self.pending: bool = data.get("pending", False) self._avatar: Optional[str] = data.get("avatar") + self._banner: Optional[str] = data.get("banner") timeout_datetime = utils.parse_time(data.get("communication_disabled_until")) self._communication_disabled_until: Optional[datetime.datetime] = timeout_datetime self._flags: int = data.get("flags", 0) @@ -413,6 +415,7 @@ def _copy(cls, member: Member) -> Self: self.activities = member.activities self._state = member._state self._avatar = member._avatar + self._banner = member._banner self._communication_disabled_until = member.current_timeout self._flags = member._flags @@ -441,6 +444,7 @@ def _update(self, data: GuildMemberUpdateEvent) -> None: self.premium_since = utils.parse_time(data.get("premium_since")) self._roles = utils.SnowflakeList(map(int, data["roles"])) self._avatar = data.get("avatar") + self._banner = data.get("banner") timeout_datetime = utils.parse_time(data.get("communication_disabled_until")) self._communication_disabled_until = timeout_datetime self._flags = data.get("flags", 0) @@ -623,6 +627,21 @@ def guild_avatar(self) -> Optional[Asset]: return None return Asset._from_guild_avatar(self._state, self.guild.id, self.id, self._avatar) + # TODO + # implement a `display_banner` property + # for more info on why this wasn't implemented read this discussion + # https://github.com/DisnakeDev/disnake/pull/1204#discussion_r1685773429 + @property + def guild_banner(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild banner + the member has. If unavailable, ``None`` is returned. + + .. versionadded:: 2.10 + """ + if self._banner is None: + return None + return Asset._from_guild_banner(self._state, self.guild.id, self.id, self._banner) + @property def activity(self) -> Optional[ActivityTypes]: """Optional[Union[:class:`BaseActivity`, :class:`Spotify`]]: Returns the primary diff --git a/disnake/types/gateway.py b/disnake/types/gateway.py index 736634c944..7f86cd5959 100644 --- a/disnake/types/gateway.py +++ b/disnake/types/gateway.py @@ -453,6 +453,7 @@ class GuildMemberUpdateEvent(TypedDict): user: User nick: NotRequired[Optional[str]] avatar: Optional[str] + banner: Optional[str] joined_at: Optional[str] premium_since: NotRequired[Optional[str]] deaf: NotRequired[bool]