jarvis-bot/jarvis/cogs/extra/calc.py

544 lines
19 KiB
Python

"""JARVIS Calculator Cog."""
from calculator import calculate
from erapi import const
from interactions import AutocompleteContext, Client, Extension, InteractionContext
from interactions.models.discord.components import Button
from interactions.models.discord.embed import Embed, EmbedField
from interactions.models.discord.enums import ButtonStyle
from interactions.models.internal.application_commands import (
OptionType,
SlashCommand,
SlashCommandChoice,
slash_option,
)
from thefuzz import process
from jarvis.data import units
from jarvis.utils import build_embed
TEMP_CHOICES = (
SlashCommandChoice(name="Fahrenheit", value=0),
SlashCommandChoice(name="Celsius", value=1),
SlashCommandChoice(name="Kelvin", value=2),
)
TEMP_LOOKUP = {0: "F", 1: "C", 2: "K"}
CURRENCY_BY_NAME = {x["name"]: x["code"] for x in const.VALID_CODES}
CURRENCY_BY_CODE = {x["code"]: x["name"] for x in const.VALID_CODES}
class CalcCog(Extension):
"""Calculator functions for JARVIS"""
def __init__(self, bot: Client):
self.bot = bot
async def _get_currency_conversion(self, from_: str, to: str) -> int:
"""Get the conversion rate."""
return self.bot.erapi.get_conversion_rate(from_, to)
calc = SlashCommand(name="calc", description="Calculate some things")
@calc.subcommand(sub_cmd_name="math", sub_cmd_description="Do a basic math calculation")
@slash_option(
name="expression",
description="Expression to calculate",
required=True,
opt_type=OptionType.STRING,
)
async def _calc_math(self, ctx: InteractionContext, expression: str) -> None:
if expression == "The answer to life, the universe, and everything":
fields = (
EmbedField(name="Expression", value=f"`{expression}`"),
EmbedField(name="Result", value=str(42)),
)
embed = build_embed(title="Calculator", description=None, fields=fields)
components = Button(
style=ButtonStyle.DANGER,
emoji="🗑️",
custom_id=f"delete|{ctx.author.id}",
)
await ctx.send(embeds=embed, components=components)
return
try:
value = calculate(expression)
except Exception as e:
await ctx.send(f"Failed to calculate:\n{e}", ephemeral=True)
return
if not value:
await ctx.send("No value? Try a valid expression", ephemeral=True)
return
fields = (
EmbedField(name="Expression", value=f"`{expression}`"),
EmbedField(name="Result", value=str(value)),
)
embed = build_embed(title="Calculator", description=None, fields=fields)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}")
await ctx.send(embeds=embed, components=components)
convert = calc.group(name="convert", description="Conversion helpers")
@convert.subcommand(sub_cmd_name="temperature", sub_cmd_description="Convert between temperatures")
@slash_option(
name="value",
description="Value to convert",
required=True,
opt_type=OptionType.NUMBER,
)
@slash_option(
name="from_unit",
description="From unit",
required=True,
opt_type=OptionType.INTEGER,
choices=TEMP_CHOICES,
)
@slash_option(
name="to_unit",
description="To unit",
required=True,
opt_type=OptionType.INTEGER,
choices=TEMP_CHOICES,
)
async def _calc_convert_temperature(
self, ctx: InteractionContext, value: int, from_unit: int, to_unit: int
) -> None:
if from_unit == to_unit:
converted = value
elif from_unit == 0:
converted = (value - 32) * (5 / 9)
if to_unit == 2:
converted += 273.15
elif from_unit == 1:
if to_unit == 0:
converted = (value * 9 / 5) + 32
else:
converted = value + 273.15
else:
converted = value + 273.15
if to_unit == 0:
converted = (value * 9 / 5) + 32
fields = (
EmbedField(name=f"°{TEMP_LOOKUP.get(from_unit)}", value=f"{value:0.2f}"),
EmbedField(name=f"°{TEMP_LOOKUP.get(to_unit)}", value=f"{converted:0.2f}"),
)
embed = build_embed(
title="Conversion",
description=f"°{TEMP_LOOKUP.get(from_unit)} -> °{TEMP_LOOKUP.get(to_unit)}",
fields=fields,
)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}")
await ctx.send(embeds=embed, components=components)
@convert.subcommand(
sub_cmd_name="currency",
sub_cmd_description="Convert currency based on current rates",
)
@slash_option(
name="value",
description="Value of starting currency",
required=True,
opt_type=OptionType.NUMBER,
)
@slash_option(
name="from_currency",
description="Currency to convert from",
required=True,
opt_type=OptionType.STRING,
autocomplete=True,
)
@slash_option(
name="to_currency",
description="Currency to convert to",
required=True,
opt_type=OptionType.STRING,
autocomplete=True,
)
async def _calc_convert_currency(
self, ctx: InteractionContext, value: int, from_currency: str, to_currency: str
) -> None:
if from_currency == to_currency:
conv = value
else:
rate = await self._get_currency_conversion(from_currency, to_currency)
conv = value * rate
fields = (
EmbedField(
name="Conversion Rate",
value=f"1 {from_currency} ~= {rate:0.4f} {to_currency}",
),
EmbedField(
name=f"{CURRENCY_BY_CODE[from_currency]} ({from_currency})",
value=f"{value:0.2f}",
),
EmbedField(
name=f"{CURRENCY_BY_CODE[to_currency]} ({to_currency})",
value=f"{conv:0.2f}",
),
)
embed = build_embed(
title="Conversion",
description=f"{from_currency} -> {to_currency}",
fields=fields,
)
components = Button(
style=ButtonStyle.DANGER,
emoji="🗑️",
custom_id=f"delete|{ctx.author.id}",
)
await ctx.send(embeds=embed, components=components)
async def _convert(self, ctx: InteractionContext, from_: str, to: str, value: int) -> Embed:
*_, which = ctx.invoke_target.split(" ")
which = getattr(units, which.capitalize(), None)
ratio = which.get_rate(from_, to)
converted = value / ratio
fields = (
EmbedField(name=from_, value=f"{value:0.2f}"),
EmbedField(name=to, value=f"{converted:0.2f}"),
)
embed = build_embed(title="Conversion", description=f"{from_} -> {to}", fields=fields)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}")
await ctx.send(embeds=embed, components=components)
@convert.subcommand(sub_cmd_name="angle", sub_cmd_description="Convert angles")
@slash_option(
name="value",
description="Angle to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_angle(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="area", sub_cmd_description="Convert areas")
@slash_option(
name="value",
description="Area to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_area(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="data", sub_cmd_description="Convert data sizes")
@slash_option(
name="value",
description="Data size to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_data(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="energy", sub_cmd_description="Convert energy")
@slash_option(
name="value",
description="Energy to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_energy(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="length", sub_cmd_description="Convert lengths")
@slash_option(
name="value",
description="Length to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_length(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="power", sub_cmd_description="Convert powers")
@slash_option(
name="value",
description="Power to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_power(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="pressure", sub_cmd_description="Convert pressures")
@slash_option(
name="value",
description="Pressure to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_pressure(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="speed", sub_cmd_description="Convert speeds")
@slash_option(
name="value",
description="Speed to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_speed(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="time", sub_cmd_description="Convert times")
@slash_option(
name="value",
description="Time to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_time(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="volume", sub_cmd_description="Convert volumes")
@slash_option(
name="value",
description="Volume to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_volume(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="weight", sub_cmd_description="Convert weights")
@slash_option(
name="value",
description="Weight to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option(
name="from_unit",
description="Units to convert from",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
@slash_option(
name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
)
async def _calc_convert_weight(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None:
await self._convert(ctx, from_unit, to_unit, value)
def _unit_autocomplete(self, which: units.Converter, unit: str) -> list[dict[str, str]]:
options = list(which.CONVERSIONS.keys())
results = process.extract(unit, options, limit=25)
if any(r[1] > 0 for r in results):
return [{"name": r[0], "value": r[0]} for r in results if r[1] > 50]
return [{"name": r[0], "value": r[0]} for r in results]
@_calc_convert_angle.autocomplete("from_unit")
@_calc_convert_area.autocomplete("from_unit")
@_calc_convert_data.autocomplete("from_unit")
@_calc_convert_energy.autocomplete("from_unit")
@_calc_convert_length.autocomplete("from_unit")
@_calc_convert_power.autocomplete("from_unit")
@_calc_convert_pressure.autocomplete("from_unit")
@_calc_convert_speed.autocomplete("from_unit")
@_calc_convert_time.autocomplete("from_unit")
@_calc_convert_volume.autocomplete("from_unit")
@_calc_convert_weight.autocomplete("from_unit")
async def _autocomplete_from_unit(self, ctx: AutocompleteContext) -> None:
*_, which = ctx.invoke_target.split(" ")
which = getattr(units, which.capitalize(), None)
await ctx.send(choices=self._unit_autocomplete(which, ctx.input_text))
@_calc_convert_angle.autocomplete("to_unit")
@_calc_convert_area.autocomplete("to_unit")
@_calc_convert_data.autocomplete("to_unit")
@_calc_convert_energy.autocomplete("to_unit")
@_calc_convert_length.autocomplete("to_unit")
@_calc_convert_power.autocomplete("to_unit")
@_calc_convert_pressure.autocomplete("to_unit")
@_calc_convert_speed.autocomplete("to_unit")
@_calc_convert_time.autocomplete("to_unit")
@_calc_convert_volume.autocomplete("to_unit")
@_calc_convert_weight.autocomplete("to_unit")
async def _autocomplete_to_unit(self, ctx: AutocompleteContext) -> None:
*_, which = ctx.invoke_target.split(" ")
which = getattr(units, which.capitalize(), None)
await ctx.send(choices=self._unit_autocomplete(which, ctx.input_text))
def _currency_autocomplete(self, currency: str) -> list[dict[str, str]]:
lookup_name = {f"{k} ({v})": v for k, v in CURRENCY_BY_NAME.items()}
lookup_value = {v: k for k, v in lookup_name.items()}
results_name = process.extract(currency, list(lookup_name.keys()), limit=25)
results_value = process.extract(currency, list(lookup_value.keys()), limit=25)
results = {}
for r in results_value + results_name:
name = r[0]
if len(name) == 3:
name = lookup_value[name]
if name not in results:
results[name] = r[1]
if r[1] > results[name]:
results[name] = r[1]
results = sorted(results.items(), key=lambda x: -x[1])[:10]
if any(r[1] > 0 for r in results):
return [{"name": r[0], "value": lookup_name[r[0]]} for r in results if r[1]]
return [{"name": r[0], "value": lookup_name[r[0]]} for r in results]
@_calc_convert_currency.autocomplete("from_currency")
async def _autocomplete_from_currency(self, ctx: AutocompleteContext) -> None:
await ctx.send(choices=self._currency_autocomplete(ctx.input_text))
@_calc_convert_currency.autocomplete("to_currency")
async def _autocomplete_to_currency(self, ctx: AutocompleteContext) -> None:
await ctx.send(choices=self._currency_autocomplete(ctx.input_text))
def setup(bot: Client) -> None:
"""Add CalcCog to JARVIS"""
CalcCog(bot)