Last active
August 29, 2025 20:06
-
-
Save Soheab/cf356c62a6134508869bf40640b04856 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import annotations | |
from typing import Any, Literal, Self, overload | |
import asyncio | |
import datetime | |
import secrets | |
import aiohttp | |
import discord | |
from discord.ext import commands | |
type ValidMediaType = ( | |
str | |
| Media | |
| discord.MediaGalleryItem | |
| discord.UnfurledMediaItem | |
| discord.ui.Thumbnail[Any] | |
| discord.Asset | |
| discord.File | |
) | |
class AuthorFooter: | |
def __init__( | |
self, | |
*, | |
text: str, | |
icon: ValidMediaType | None = None, | |
) -> None: | |
self.text: str = text | |
self.icon: Media | None = Media.to_self(icon) | |
async def __icon_as_emoji(self, client: discord.Client | commands.Bot) -> str | None: | |
if not self.icon: | |
return None | |
def schedule_emoji_deletion(emoji: discord.Emoji) -> None: | |
async def delete_emoji(): | |
await asyncio.sleep(10) | |
await emoji.delete() | |
asyncio.create_task(delete_emoji()) | |
unique_name = f"embed_icon_{secrets.token_hex(4)}" | |
async with aiohttp.ClientSession() as session: | |
async with session.get(self.icon.url) as resp: | |
if resp.status == 200: | |
emoji = await client.create_application_emoji(name=unique_name, image=await resp.read()) | |
schedule_emoji_deletion(emoji) | |
return str(emoji) | |
return None | |
async def to_item( | |
self, | |
client: discord.Client | commands.Bot | None = None, | |
*, | |
timestamp: str | None = None, | |
) -> discord.ui.TextDisplay[Any]: | |
if not client: | |
return discord.ui.TextDisplay(f"-# {self.text}") | |
icon = await self.__icon_as_emoji(client) | |
icon = f"{icon} " if icon else "" | |
final = f"-# {icon}{self.text}" | |
if timestamp: | |
final += f" | {timestamp}" | |
return discord.ui.TextDisplay(final) | |
class Media: | |
def __init__( | |
self, | |
*, | |
url: str, | |
description: str | None = None, | |
spoiler: bool = False, | |
) -> None: | |
self.url: str = url | |
self.description: str | None = description | |
self.spoiler: bool = spoiler | |
@overload | |
def to_item(self, *, title: str | None = ..., description: str | None = ...) -> discord.ui.Section[Any]: ... | |
@overload | |
def to_item( | |
self, | |
) -> discord.ui.MediaGallery[Any]: ... | |
def to_item( | |
self, *, title: str | None = None, description: str | None = None | |
) -> discord.ui.MediaGallery[Any] | discord.ui.Section[Any]: | |
if not any([title, description]): | |
return self.images_as_gallery([self]) | |
section = discord.ui.Section[Any]( | |
accessory=discord.ui.Thumbnail(media=self.url, description=self.description, spoiler=self.spoiler), | |
) | |
if title: | |
section.add_item(discord.ui.TextDisplay(title)) | |
if description: | |
section.add_item(discord.ui.TextDisplay(description)) | |
return section | |
@staticmethod | |
def images_as_gallery(images: list[Media]) -> discord.ui.MediaGallery[Any]: | |
return discord.ui.MediaGallery(*[ | |
discord.MediaGalleryItem(media=image.url, description=image.description, spoiler=image.spoiler) | |
for image in images | |
]) | |
@overload | |
@classmethod | |
def to_self( | |
cls, | |
media: Literal[None], | |
description: str | None = ..., | |
spoiler: bool = ..., | |
) -> None: ... | |
@overload | |
@classmethod | |
def to_self( | |
cls, | |
media: ValidMediaType, | |
description: str | None = ..., | |
spoiler: bool = ..., | |
) -> Self: ... | |
@classmethod | |
def to_self( | |
cls, | |
media: ValidMediaType | None, | |
description: str | None = None, | |
spoiler: bool = False, | |
) -> Self | None: | |
if media is None: | |
return None | |
if isinstance(media, cls): | |
return media | |
kwargs: dict[str, Any] = { | |
"description": description, | |
"spoiler": spoiler, | |
} | |
if isinstance(media, str): | |
kwargs["url"] = media | |
elif isinstance(media, discord.File): | |
kwargs["url"] = media.uri | |
elif isinstance(media, (discord.MediaGalleryItem, discord.ui.Thumbnail)): | |
kwargs["url"] = media.media.url | |
kwargs["description"] = media.description or description | |
kwargs["spoiler"] = media.spoiler or spoiler | |
elif isinstance(media, (discord.UnfurledMediaItem, discord.Asset)): | |
kwargs["url"] = media.url | |
else: | |
raise ValueError(f"Invalid media item type: {type(media)}") | |
if not kwargs.get("url"): | |
return None | |
return cls(**kwargs) | |
class Field: | |
@overload | |
def __init__( | |
self, | |
*, | |
name: str, | |
value: str, | |
images: list[ValidMediaType] | None = ..., | |
add_separator: Literal[True] = ..., | |
separator_visible: bool = ..., | |
separator_spacing: discord.SeparatorSpacing = ..., | |
) -> None: ... | |
@overload | |
def __init__( | |
self, | |
*, | |
name: str, | |
value: str, | |
images: list[ValidMediaType] | None = ..., | |
add_separator: Literal[False] = ..., | |
) -> None: ... | |
@overload | |
def __init__( | |
self, | |
*, | |
name: str, | |
value: str, | |
add_separator: Literal[True] = ..., | |
separator_visible: bool = ..., | |
separator_spacing: discord.SeparatorSpacing = ..., | |
button: discord.ui.Button[Any] | None = ..., | |
thumbnail: ValidMediaType | None = ..., | |
) -> None: ... | |
@overload | |
def __init__( | |
self, | |
*, | |
name: str, | |
value: str, | |
add_separator: Literal[True] = ..., | |
button: discord.ui.Button[Any] | None = ..., | |
thumbnail: ValidMediaType | None = ..., | |
) -> None: ... | |
@overload | |
def __init__( | |
self, | |
*, | |
name: str, | |
value: str, | |
button: discord.ui.Button[Any] | None = ..., | |
thumbnail: ValidMediaType | None = ..., | |
images: list[ValidMediaType] | None = ..., | |
add_separator: bool = ..., | |
separator_visible: bool = ..., | |
separator_spacing: discord.SeparatorSpacing = ..., | |
) -> None: ... | |
def __init__( | |
self, | |
*, | |
name: str, | |
value: str, | |
button: discord.ui.Button[Any] | None = None, | |
thumbnail: ValidMediaType | None = None, | |
images: list[ValidMediaType] | None = None, | |
add_separator: bool = True, | |
separator_visible: bool = True, | |
separator_spacing: discord.SeparatorSpacing = discord.SeparatorSpacing.small, | |
) -> None: | |
if any([button, thumbnail]) and images: | |
raise ValueError("Cannot set both `button` or `thumbnail` and `images`. Use one or the other.") | |
self.name: str = name | |
self.value: str = value | |
self.add_separator: bool = add_separator | |
self.separator_visible: bool = separator_visible | |
self.separator_spacing: discord.SeparatorSpacing = separator_spacing | |
self.button: discord.ui.Button[Any] | None = button | |
self.thumbnail: Media | None = Media.to_self(thumbnail) | |
self.images: list[Media] = [Media.to_self(image) for image in images] if images else [] | |
def set_button(self, button: discord.ui.Button[Any]) -> Self: | |
if any([self.thumbnail, self.images]): | |
raise ValueError("Cannot set both `button` and `thumbnail` and `images`. Use one or the other.") | |
self.button = button | |
return self | |
def set_thumbnail(self, thumbnail: ValidMediaType) -> Self: | |
if any([self.button, self.images]): | |
raise ValueError("Cannot set both `button` and `thumbnail` and `images`. Use one or the other.") | |
self.thumbnail = Media.to_self(thumbnail) | |
return self | |
def add_image(self, image: ValidMediaType) -> Self: | |
if any([self.button, self.thumbnail]): | |
raise ValueError("Cannot set both `button` or `thumbnail` and `images`. Use one or the other.") | |
self.images.append(Media.to_self(image)) | |
return self | |
def set_separator( | |
self, add: bool, visible: bool = True, spacing: discord.SeparatorSpacing = discord.SeparatorSpacing.small | |
) -> Self: | |
self.add_separator = add | |
self.separator_visible = visible | |
self.separator_spacing = spacing | |
return self | |
@property | |
def separator(self) -> discord.ui.Separator[Any] | None: | |
if not self.add_separator: | |
return None | |
return discord.ui.Separator(visible=self.separator_visible, spacing=self.separator_spacing) | |
def get_contents(self) -> list[discord.ui.TextDisplay[Any]]: | |
return [ | |
discord.ui.TextDisplay(f"### {self.name}"), | |
discord.ui.TextDisplay(self.value), | |
] | |
def to_items(self) -> list[discord.ui.TextDisplay[Any] | discord.ui.Separator[Any] | discord.ui.Section[Any]]: | |
items: list[Any] = [] | |
if not any([self.button, self.thumbnail]): | |
items.extend(self.get_contents()) | |
if self.images: | |
items.append(Media.images_as_gallery(self.images)) | |
else: | |
if self.thumbnail: | |
items.append(self.thumbnail.to_item(title=f"### {self.name}", description=self.value)) | |
elif self.button: | |
section = discord.ui.Section[Any](*self.get_contents(), accessory=self.button) | |
items.append(section) | |
if self.separator: | |
items.append(self.separator) | |
return items | |
class ContainerEmbed: | |
def __init__( | |
self, | |
*, | |
title: str | None = None, | |
url: str | None = None, | |
description: str | None = None, | |
color: discord.Color | None = discord.utils.MISSING, | |
colour: discord.Color | None = discord.utils.MISSING, | |
timestamp: str | datetime.datetime | None = None, | |
footer: str | AuthorFooter | None = None, | |
author: str | AuthorFooter | None = None, | |
thumbnail: ValidMediaType | None = None, | |
image: ValidMediaType | None = None, | |
images: list[ValidMediaType] | None = None, | |
fields: list[Field] | None = None, | |
client: discord.Client | commands.Bot | None = None, | |
) -> None: | |
super().__init__() | |
self.__client: discord.Client | commands.Bot | None = client | |
if image and images: | |
raise ValueError("Cannot set both `image` and `images`. Use one or the other.") | |
self.title = title | |
self.url = url | |
self.timestamp = timestamp | |
self.description = description | |
self.color = color if color else colour if colour else None | |
self.footer = footer | |
self.author = author | |
self.thumbnail = thumbnail | |
self.image = image | |
self.images = images | |
self._fields: list[Field] = [] | |
if fields: | |
self.fields = fields | |
async def to_container(self) -> discord.ui.Container[Any]: | |
container = discord.ui.Container[Any]( | |
accent_color=(self.color if self.color is not discord.utils.MISSING else discord.Color.default()) | |
) | |
title = self.__get_title() | |
timestamp = self.__get_timestamp() | |
if self.author: | |
container.add_item(await self.author.to_item(self.__client)) | |
if self.thumbnail: | |
container.add_item(self.thumbnail.to_item(title=title, description=self.description)) | |
else: | |
if title: | |
container.add_item(discord.ui.TextDisplay(title)) | |
if self.description: | |
container.add_item(discord.ui.TextDisplay(self.description)) | |
if self.fields: | |
for field in self.fields: | |
for component in field.to_items(): | |
container.add_item(component) | |
if self.image: | |
container.add_item(self.image.to_item()) | |
elif self.images: | |
container.add_item(Media.images_as_gallery(self.images)) | |
if self.footer: | |
container.add_item(await self.footer.to_item(self.__client, timestamp=timestamp)) | |
elif timestamp: | |
container.add_item(discord.ui.TextDisplay(timestamp)) | |
return container | |
@classmethod | |
def from_embed( | |
cls, | |
embed: discord.Embed, | |
/, | |
*, | |
client: discord.Client | commands.Bot | None = None, | |
) -> Self: | |
instance = cls( | |
client=client, | |
title=embed.title, | |
description=embed.description, | |
color=embed.color, | |
footer=(AuthorFooter(text=embed.footer.text, icon=embed.footer.icon_url) if embed.footer.text else None), | |
author=(AuthorFooter(text=embed.author.name, icon=embed.author.icon_url) if embed.author.name else None), | |
thumbnail=(Media(url=embed.thumbnail.url) if embed.thumbnail.url else None), | |
image=(Media(url=embed.image.url) if embed.image.url else None), | |
url=embed.url, | |
timestamp=embed.timestamp, | |
) | |
return instance | |
def __get_title(self) -> str | None: | |
title = self.title | |
if not title: | |
return None | |
url = self.url | |
if url: | |
return f"## [{title}]({url})" | |
return f"## {title}" | |
def __get_timestamp(self) -> str | None: | |
timestamp = self.timestamp | |
if not timestamp: | |
return None | |
if isinstance(timestamp, datetime.datetime): | |
return timestamp.strftime("%d/%m/%Y %H:%M") | |
return timestamp | |
@property | |
def title(self) -> str | None: | |
return getattr(self, "_title", None) | |
@title.setter | |
def title(self, value: str | None) -> None: | |
if value is None: | |
self._title = None | |
else: | |
self._title = str(value) | |
@property | |
def description(self) -> str | None: | |
return getattr(self, "_description", None) | |
@description.setter | |
def description(self, value: str | None) -> None: | |
if value is None: | |
self._description = None | |
else: | |
self._description = str(value) | |
@property | |
def color(self) -> discord.Color | None: | |
return getattr(self, "_color", None) | |
@color.setter | |
def color(self, value: discord.Color | int | None) -> None: | |
if value is None: | |
self._color = None | |
elif isinstance(value, discord.Color): | |
self._color = value | |
elif isinstance(value, int): | |
self._color = discord.Color(value) | |
else: | |
raise TypeError("Invalid type for color. Expected discord.Color, int or None.") | |
colour = color | |
@property | |
def timestamp(self) -> datetime.datetime | None: | |
return getattr(self, "_timestamp", None) | |
@timestamp.setter | |
def timestamp(self, value: str | datetime.datetime | None) -> None: | |
if value is None: | |
self._timestamp = None | |
else: | |
if isinstance(value, str): | |
self._timestamp = datetime.datetime.fromisoformat(value) | |
elif isinstance(value, datetime.datetime): | |
self._timestamp = value | |
else: | |
raise TypeError("Invalid type for timestamp. Expected str, datetime.datetime or None.") | |
@property | |
def url(self) -> str | None: | |
return getattr(self, "_url", None) | |
@url.setter | |
def url(self, value: str | None) -> None: | |
if value is None: | |
self._url = None | |
else: | |
self._url = str(value) | |
@property | |
def author(self) -> AuthorFooter | None: | |
return getattr(self, "_author", None) | |
@author.setter | |
def author(self, value: str | AuthorFooter | None) -> None: | |
if value is None: | |
self._author = None | |
elif isinstance(value, AuthorFooter): | |
self._author = value | |
else: | |
self._author = AuthorFooter(text=value) | |
@property | |
def footer(self) -> AuthorFooter | None: | |
return getattr(self, "_footer", None) | |
@footer.setter | |
def footer(self, value: str | AuthorFooter | None) -> None: | |
if value is None: | |
self._footer = None | |
elif isinstance(value, AuthorFooter): | |
self._footer = value | |
else: | |
self._footer = AuthorFooter(text=value) | |
@property | |
def image(self) -> Media | None: | |
return getattr(self, "_image", None) | |
@image.setter | |
def image( | |
self, | |
value: ValidMediaType | None, | |
) -> None: | |
if value is None: | |
self._image = None | |
else: | |
if self.images: | |
raise ValueError("Cannot set both `image` and `images`. Use one or the other.") | |
self._image = Media.to_self(value) | |
@property | |
def images(self) -> list[Media]: | |
return getattr(self, "_images", []) | |
@images.setter | |
def images(self, value: list[ValidMediaType] | None) -> None: | |
if value is None: | |
self._images = [] | |
else: | |
if self.image: | |
raise ValueError("Cannot set both `image` and `images`. Use one or the other.") | |
self._images = [Media.to_self(item) for item in value] | |
@property | |
def thumbnail(self) -> Media | None: | |
return getattr(self, "_thumbnail", None) | |
@thumbnail.setter | |
def thumbnail( | |
self, | |
value: ValidMediaType | None, | |
) -> None: | |
if value is None: | |
self._thumbnail = None | |
else: | |
self._thumbnail = Media.to_self(value) | |
@property | |
def fields(self) -> list[Field]: | |
return getattr(self, "_fields", []) | |
@fields.setter | |
def fields(self, value: list[Field]) -> None: | |
if not value: | |
self._fields = [] | |
return | |
if not all(isinstance(field, Field) for field in value): | |
raise TypeError("All items in fields must be instances of Field.") | |
self._fields = value | |
def set_image( | |
self, | |
media: ValidMediaType | None = discord.utils.MISSING, | |
*, | |
description: str | None = None, | |
spoiler: bool = False, | |
url: str | None = discord.utils.MISSING, | |
) -> Self: | |
media = url or media | |
if media is discord.utils.MISSING: | |
raise ValueError("You must provide a media to set or None to remove it.") | |
if not media: | |
self._image = None | |
return self | |
if self.images: | |
raise ValueError("Cannot set both `image` and `images`. Use one or the other.") | |
self._image = Media.to_self(media, description=description, spoiler=spoiler) | |
return self | |
def add_image( | |
self, | |
media: ValidMediaType, | |
*, | |
description: str | None = None, | |
spoiler: bool = False, | |
) -> Self: | |
if self.image: | |
raise ValueError("Cannot set both `image` and `images`. Use one or the other.") | |
self._images.append(Media.to_self(media, description=description, spoiler=spoiler)) | |
return self | |
def set_thumbnail( | |
self, | |
media: ValidMediaType | None = discord.utils.MISSING, | |
*, | |
description: str | None = None, | |
spoiler: bool = False, | |
url: str | None = discord.utils.MISSING, | |
) -> Self: | |
media = url or media | |
if media is discord.utils.MISSING: | |
raise ValueError("You must provide a media to set or None to remove it.") | |
if not media: | |
self._thumbnail = None | |
return self | |
self._thumbnail = Media.to_self(media, description=description, spoiler=spoiler) | |
return self | |
def set_footer( | |
self, | |
*, | |
text: str | None = discord.utils.MISSING, | |
icon: ValidMediaType | None = None, | |
# compat with discord.Embed | |
icon_url: ValidMediaType | None = discord.utils.MISSING, | |
) -> Self: | |
icon = icon_url or icon | |
if not text: | |
self._footer = None | |
return self | |
self._footer = AuthorFooter(text=text, icon=icon) | |
return self | |
def set_author( | |
self, | |
*, | |
text: str | None = discord.utils.MISSING, | |
icon: ValidMediaType | None = None, | |
# compat with discord.Embed | |
name: str | None = discord.utils.MISSING, | |
icon_url: ValidMediaType | None = discord.utils.MISSING, | |
) -> Self: | |
text = name or text | |
icon = icon_url or icon | |
if not text: | |
self._author = None | |
return self | |
self._author = AuthorFooter(text=text, icon=icon) | |
return self | |
def append_field(self, field: Field) -> Self: | |
if not isinstance(field, Field): | |
raise TypeError("field must be an instance of Field.") | |
self._fields.append(field) | |
return self | |
def clear_fields(self) -> Self: | |
self._fields.clear() | |
return self | |
@overload | |
def add_field( | |
self, | |
*, | |
name: str, | |
value: str, | |
add_separator: Literal[True] = ..., | |
separator_visible: bool = ..., | |
separator_spacing: discord.SeparatorSpacing = ..., | |
button: discord.ui.Button[Any] | None = ..., | |
thumbnail: ValidMediaType | None = ..., | |
images: list[ValidMediaType] | None = ..., | |
inline: bool = ..., | |
) -> Self: ... | |
@overload | |
def add_field( | |
self, | |
*, | |
name: str, | |
value: str, | |
add_separator: Literal[False] = ..., | |
button: discord.ui.Button[Any] | None = ..., | |
thumbnail: ValidMediaType | None = ..., | |
inline: bool = ..., | |
) -> Self: ... | |
@overload | |
def add_field( | |
self, | |
*, | |
name: str, | |
value: str, | |
add_separator: Literal[False] = ..., | |
images: list[ValidMediaType] | None = ..., | |
inline: bool = ..., | |
) -> Self: ... | |
@overload | |
def add_field( | |
self, | |
*, | |
name: str, | |
value: str, | |
add_separator: Literal[True] = ..., | |
separator_visible: bool = ..., | |
separator_spacing: discord.SeparatorSpacing = ..., | |
images: list[ValidMediaType] | None = ..., | |
inline: bool = ..., | |
) -> Self: ... | |
@overload | |
def add_field( | |
self, | |
*, | |
name: str, | |
value: str, | |
images: list[ValidMediaType] | None = ..., | |
inline: bool = ..., | |
) -> Self: ... | |
@overload | |
def add_field( | |
self, | |
*, | |
name: str, | |
value: str, | |
button: discord.ui.Button[Any] | None = ..., | |
thumbnail: ValidMediaType | None = ..., | |
inline: bool = ..., | |
) -> Self: ... | |
def add_field( | |
self, | |
*, | |
name: str, | |
value: str, | |
add_separator: bool = True, | |
separator_visible: bool = True, | |
separator_spacing: discord.SeparatorSpacing = discord.SeparatorSpacing.small, | |
button: discord.ui.Button[Any] | None = None, | |
thumbnail: ValidMediaType | None = None, | |
images: list[ValidMediaType] | None = None, | |
inline: bool = False, | |
) -> Self: | |
return self.append_field( | |
Field( | |
name=name, | |
value=value, | |
add_separator=add_separator, | |
separator_visible=separator_visible, | |
separator_spacing=separator_spacing, | |
button=button, | |
thumbnail=thumbnail, | |
images=images, | |
) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment