Skip to content

Instantly share code, notes, and snippets.

@Soheab
Last active August 29, 2025 20:06
Show Gist options
  • Save Soheab/cf356c62a6134508869bf40640b04856 to your computer and use it in GitHub Desktop.
Save Soheab/cf356c62a6134508869bf40640b04856 to your computer and use it in GitHub Desktop.
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