Skip to content

Instantly share code, notes, and snippets.

@Soheab
Last active November 8, 2025 21:55
Show Gist options
  • Select an option

  • Save Soheab/cf356c62a6134508869bf40640b04856 to your computer and use it in GitHub Desktop.

Select an option

Save Soheab/cf356c62a6134508869bf40640b04856 to your computer and use it in GitHub Desktop.
from __future__ import annotations
import datetime
from typing import Any, Literal, Self, overload
import discord
type ValidMediaType = (
str
| Media
| discord.MediaGalleryItem
| discord.UnfurledMediaItem
| discord.ui.Thumbnail[Any]
| discord.Asset
| discord.File
)
class AuthorFooter:
def __init__(
self,
*,
text: str,
icon: str | None = None,
) -> None:
self.text: str = text
self.icon: str | None = icon
def to_item(
self,
*,
timestamp: str | None = None,
) -> discord.ui.TextDisplay[Any]:
icon = f"{self.icon} " if self.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,
) -> None:
super().__init__()
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
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(self.author.to_item())
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(self.footer.to_item(timestamp=timestamp))
elif timestamp:
container.add_item(discord.ui.TextDisplay(timestamp))
return container
def as_view(self, timeout: int | float | None = 180.0) -> discord.ui.LayoutView:
return discord.ui.LayoutView(timeout=timeout).add_item(self.to_container())
@classmethod
def from_embed(
cls,
embed: discord.Embed,
/,
) -> Self:
instance = cls(
title=embed.title,
description=embed.description,
color=embed.color,
footer=(
AuthorFooter(text=embed.footer.text) if embed.footer.text else None
),
author=(
AuthorFooter(text=embed.author.name) 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: str | None = None,
# compat with discord.Embed
icon_url: str | 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: str | None = None,
# compat with discord.Embed
name: str | None = discord.utils.MISSING,
icon_url: str | 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,
# compat with discord.Embed
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