import json
from inspect import ismethod
from typing import Optional, Union
from discord import Colour, Embed
from ..exceptions import BadColourArgument, EmbedParseError
from ..interface import Block
from ..interpreter import Context
from .helpers import helper_split, implicit_bool
def string_to_color(argument: str) -> Colour:
arg = argument.replace("0x", "").lower()
if arg[0] == "#":
arg = arg[1:]
try:
value = int(arg, base=16)
if not (0 <= value <= 0xFFFFFF):
raise BadColourArgument(arg)
return Colour(value=value)
except ValueError:
arg = arg.replace(" ", "_")
method = getattr(Colour, arg, None)
if arg.startswith("from_") or method is None or not ismethod(method):
raise BadColourArgument(arg)
return method()
def set_color(embed: Embed, attribute: str, value: str):
value = string_to_color(value)
setattr(embed, attribute, value)
def set_dynamic_url(embed: Embed, attribute: str, value: str):
method = getattr(embed, f"set_{attribute}")
method(url=value)
def add_field(embed: Embed, _: str, payload: str):
if (data := helper_split(payload, 3)) is None:
raise EmbedParseError("`add_field` payload was not split by |")
try:
name, value, _inline = data
inline = implicit_bool(_inline)
if inline is None:
raise EmbedParseError(
"`inline` argument for `add_field` is not a boolean value (_inline)"
)
except ValueError:
name, value = helper_split(payload, 2)
inline = False
embed.add_field(name=name, value=value, inline=inline)
def set_footer(embed: Embed, _: str, payload: str):
data = helper_split(payload, 2)
if data is None:
embed.set_footer(text=payload)
else:
text, icon_url = data
embed.set_footer(text=text, icon_url=icon_url)
[docs]class EmbedBlock(Block):
"""
An embed block will send an embed in the tag response.
There are two ways to use the embed block, either by using properly
formatted embed JSON from an embed generator or manually inputting
the accepted embed attributes.
**JSON**
Using JSON to create an embed offers complete embed customization.
Multiple embed generators are available online to visualize and generate
embed JSON.
**Usage:** ``{embed(<json>)}``
**Payload:** None
**Parameter:** json
**Examples:** ::
{embed({"title":"Hello!", "description":"This is a test embed."})}
{embed({
"title":"Here's a random duck!",
"image":{"url":"https://random-d.uk/api/randomimg"},
"color":15194415
})}
**Manual**
The following embed attributes can be set manually:
* ``title``
* ``description``
* ``color``
* ``url``
* ``thumbnail``
* ``image``
* ``footer``
* ``field`` - (See below)
Adding a field to an embed requires the payload to be split by ``|``, into
either 2 or 3 parts. The first part is the name of the field, the second is
the text of the field, and the third optionally specifies whether the field
should be inline.
**Usage:** ``{embed(<attribute>):<value>}``
**Payload:** value
**Parameter:** attribute
**Examples:** ::
{embed(color):#37b2cb}
{embed(title):Rules}
{embed(description):Follow these rules to ensure a good experience in our server!}
{embed(field):Rule 1|Respect everyone you speak to.|false}
{embed(footer):Thanks for reading!|{guild(icon)}}
Both methods can be combined to create an embed in a tag.
The following tagscript uses JSON to create an embed with fields and later
set the embed title.
::
{embed({{"fields":[{"name":"Field 1","value":"field description","inline":false}]})}
{embed(title):my embed title}
"""
ACCEPTED_NAMES = ("embed",)
ATTRIBUTE_HANDLERS = {
"description": setattr,
"title": setattr,
"color": set_color,
"colour": set_color,
"url": setattr,
"thumbnail": set_dynamic_url,
"image": set_dynamic_url,
"field": add_field,
"footer": set_footer,
}
@staticmethod
def get_embed(ctx: Context) -> Embed:
return ctx.response.actions.get("embed", Embed())
@staticmethod
def value_to_color(value: Optional[Union[int, str]]) -> Colour:
if value is None or isinstance(value, Colour):
return value
if isinstance(value, int):
return Colour(value)
elif isinstance(value, str):
return string_to_color(value)
else:
raise EmbedParseError("Received invalid type for color key (expected string or int)")
def text_to_embed(self, text: str) -> Embed:
try:
data = json.loads(text)
except json.decoder.JSONDecodeError as error:
raise EmbedParseError(error) from error
if data.get("embed"):
data = data["embed"]
if data.get("timestamp"):
data["timestamp"] = data["timestamp"].strip("Z")
color = data.pop("color", data.pop("colour", None))
try:
embed = Embed.from_dict(data)
except Exception as error:
raise EmbedParseError(error) from error
else:
if color := self.value_to_color(color):
embed.color = color
return embed
@classmethod
def update_embed(cls, embed: Embed, attribute: str, value: str) -> Embed:
handler = cls.ATTRIBUTE_HANDLERS[attribute]
try:
handler(embed, attribute, value)
except Exception as error:
raise EmbedParseError(error) from error
return embed
@staticmethod
def return_error(error: Exception) -> str:
return f"Embed Parse Error: {error}"
@staticmethod
def return_embed(ctx: Context, embed: Embed) -> str:
try:
length = len(embed)
except Exception as error:
return str(error)
if length > 6000:
return f"`MAX EMBED LENGTH REACHED ({length}/6000)`"
ctx.response.actions["embed"] = embed
return ""
def process(self, ctx: Context) -> Optional[str]:
if not ctx.verb.parameter:
return self.return_embed(ctx, self.get_embed(ctx))
lowered = ctx.verb.parameter.lower()
try:
if ctx.verb.parameter.startswith("{") and ctx.verb.parameter.endswith("}"):
embed = self.text_to_embed(ctx.verb.parameter)
elif lowered in self.ATTRIBUTE_HANDLERS and ctx.verb.payload:
embed = self.get_embed(ctx)
embed = self.update_embed(embed, lowered, ctx.verb.payload)
else:
return
except EmbedParseError as error:
return self.return_error(error)
return self.return_embed(ctx, embed)