544 lines
19 KiB
Python
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)
|