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:
"""
Converts a string to a discord.Colour object
"""
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) # pylint: disable=raise-missing-from
return method()
def set_color(embed: Embed, attribute: str, value: str) -> None:
"""
Sets the colour of the embed given
"""
value = string_to_color(value)
setattr(embed, attribute, value)
def set_dynamic_url(embed: Embed, attribute: str, value: str) -> None:
"""
Dynamically sets the url of the embed
"""
method = getattr(embed, f"set_{attribute}")
method(url=value)
def add_field(embed: Embed, _: str, payload: str) -> None:
"""
Adds a field to the embed
"""
try:
name, value, _inline = helper_split(payload, 3)
inline = implicit_bool(_inline)
if inline is None:
raise EmbedParseError(
"`inline` argument for `add_field` is not a boolean value (_inline)"
)
except ValueError:
try:
name, value = helper_split(payload, 2)
except ValueError as exc:
raise EmbedParseError("`add_field` payload was not split by |") from exc
inline = False
embed.add_field(name=name, value=value, inline=inline)
[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``
.. tagscript::
{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``
* ``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``
.. tagscript::
{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}
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.
:: tagscript::
{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,
}
[docs] @staticmethod
def get_embed(ctx: Context) -> Embed:
"""
Gets the embed object
"""
return ctx.response.actions.get("embed", Embed())
[docs] @staticmethod
def value_to_color(value: Optional[Union[int, str]]) -> Colour:
"""
Converts a value to a discord.Colour object
"""
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)")
[docs] def text_to_embed(self, text: str) -> Embed:
"""
Converts json to an 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
[docs] @classmethod
def update_embed(cls, embed: Embed, attribute: str, value: str) -> Embed:
"""
Update the embed with all attributes
"""
handler = cls.ATTRIBUTE_HANDLERS[attribute]
try:
handler(embed, attribute, value)
except Exception as error:
raise EmbedParseError(error) from error
return embed
[docs] @staticmethod
def return_error(error: Exception) -> str:
"""
Return an error message
"""
return f"Embed Parse Error: {error}"
[docs] @staticmethod
def return_embed(ctx: Context, embed: Embed) -> str:
"""
Returns the embed
"""
try:
length = len(embed)
except Exception as error: # pylint: disable=broad-except
return str(error)
if length > 6000:
return f"`MAX EMBED LENGTH REACHED ({length}/6000)`"
ctx.response.actions["embed"] = embed
return ""
[docs] def process(self, ctx: Context) -> Optional[str]:
"""
Process the block
"""
if not ctx.verb.parameter:
return self.return_embed(ctx, self.get_embed(ctx))
lowered = ctx.verb.parameter.lower()
try:
if ctx.verb.parameter.strip().startswith("{") and ctx.verb.parameter.strip().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)