diff --git a/cogs/copypasta.py b/cogs/copypasta.py index 97f0462..45a03e2 100644 --- a/cogs/copypasta.py +++ b/cogs/copypasta.py @@ -111,6 +111,11 @@ I will love, cherish, and protect Yuuka until my very last breath; my successors ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠛⣳⡾⢯⣿⣷⣶⣦⣤⣤⣤⣤⠤⡶⢶⣞⡛⠛⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀""" await ctx.reply(file=discord.File(io.StringIO(pasta), filename="yuyuko.txt")) + @commands.command() + async def anlaki(self, ctx): + pasta = r"""bro anlaki bro anlaki you guys are dogshit bro anlaki bro anlaki. You guys have; NO hands, NO brain, I'm so happy that I have monitor I have mouse I have keyboard uhh I have body I have hands I have arms... feet? Like bro...""" + await ctx.reply(pasta) + async def setup(bot): await bot.add_cog(Copypasta(bot)) diff --git a/cogs/currency_converter.py b/cogs/currency_converter.py index dfca605..21df741 100644 --- a/cogs/currency_converter.py +++ b/cogs/currency_converter.py @@ -3,81 +3,141 @@ import time import json import aiohttp import discord +from discord import app_commands from discord.ext import commands # load aliases from json with open(os.path.join(os.path.dirname(__file__), 'currency_aliases.json'), 'r', encoding='utf-8') as f: _ALIAS_MAP = json.load(f) -# in‑memory cache for rates: {base: (timestamp, rates_dict)} +# unique ISO codes available for autocomplete (preserves order, deduplicates) +_ISO_CODES = sorted(set(_ALIAS_MAP.values())) + +# in-memory cache for rates: {base: (timestamp, rates_dict)} _RATE_CACHE = {} _CACHE_TTL = 3600 # seconds + +def _norm(cur: str) -> str: + """Normalize a currency string to its ISO code. + Returns upper-case ISO code or raises ValueError if unknown. + """ + cur = cur.strip().lower() + if cur in _ALIAS_MAP: + return _ALIAS_MAP[cur] + cur = cur.upper() + if len(cur) == 3 and cur.isalpha(): + return cur + raise ValueError(f"unknown currency '{cur}'") + + +async def _get_rates(base: str) -> dict: + """Fetch rates for *base* from cache or the Frankfurter API.""" + now = time.time() + cached = _RATE_CACHE.get(base) + if cached and now - cached[0] < _CACHE_TTL: + return cached[1] + url = f"https://api.frankfurter.app/latest?base={base}" + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + if resp.status != 200: + raise RuntimeError('currency API error') + data = await resp.json() + rates = data.get('rates', {}) + _RATE_CACHE[base] = (now, rates) + return rates + + +def _make_embed(amount: float, from_code: str, to_code: str, rate: float) -> discord.Embed: + result = amount * rate + embed = discord.Embed(title='Currency conversion', color=0x5865F2) + embed.add_field(name='From', value=f"{amount:g} {from_code}", inline=True) + embed.add_field(name='To', value=f"{result:g} {to_code}", inline=True) + embed.add_field(name='Rate', value=f"1 {from_code} = {rate:g} {to_code}", inline=False) + embed.set_footer(text='Data from frankfurter.app') + return embed + + +async def _currency_autocomplete( + interaction: discord.Interaction, + current: str, +) -> list[app_commands.Choice[str]]: + """Return up to 25 matching currency codes/aliases for autocomplete.""" + current_lower = current.strip().lower() + results: list[app_commands.Choice[str]] = [] + + # exact alias matches first + for alias, code in _ALIAS_MAP.items(): + if current_lower in alias and len(results) < 25: + results.append(app_commands.Choice(name=f"{code} ({alias})", value=code)) + + # fill remaining slots with ISO code prefix matches + for code in _ISO_CODES: + if code.startswith(current.upper()) and len(results) < 25: + if not any(c.value == code for c in results): + results.append(app_commands.Choice(name=code, value=code)) + + return results[:25] + + class CurrencyConverter(commands.Cog): - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot - def _norm(self, cur: str) -> str: - """Normalize a currency string to its ISO code. - Returns upper‑case ISO code or raises ValueError if unknown. - """ - cur = cur.strip().lower() - if cur in _ALIAS_MAP: - return _ALIAS_MAP[cur] - cur = cur.upper() - if len(cur) == 3 and cur.isalpha(): - return cur - raise ValueError(f"unknown currency '{cur}'") + # ── slash command ──────────────────────────────────────────────────────── - async def _get_rates(self, base: str) -> dict: - """Fetch rates for *base* from cache or the Frankfurter API. - Returns a dict mapping target ISO codes to float rates. - """ - now = time.time() - cached = _RATE_CACHE.get(base) - if cached and now - cached[0] < _CACHE_TTL: - return cached[1] - url = f"https://api.frankfurter.app/latest?base={base}" - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - if resp.status != 200: - raise RuntimeError('currency API error') - data = await resp.json() - rates = data.get('rates', {}) - _RATE_CACHE[base] = (now, rates) - return rates + @app_commands.command(name='currency', description='Convert between currencies') + @app_commands.describe( + from_cur='Currency to convert from (e.g. USD, dollar, $)', + to_cur='Currency to convert to (e.g. EUR, euro, €)', + amount='Amount to convert (default: 1)', + ) + @app_commands.autocomplete(from_cur=_currency_autocomplete, to_cur=_currency_autocomplete) + async def currency_slash( + self, + interaction: discord.Interaction, + from_cur: str, + to_cur: str, + amount: float = 1.0, + ): + await interaction.response.defer() + try: + from_code = _norm(from_cur) + to_code = _norm(to_cur) + except ValueError as e: + return await interaction.followup.send(f"Unknown currency: {e}", ephemeral=True) + try: + rates = await _get_rates(from_code) + except Exception: + return await interaction.followup.send('Could not fetch conversion rates, try again later.', ephemeral=True) + rate = rates.get(to_code) + if rate is None: + return await interaction.followup.send(f"Unsupported conversion: {from_code} to {to_code}", ephemeral=True) + await interaction.followup.send(embed=_make_embed(amount, from_code, to_code, rate)) + + # ── prefix command (kept for backwards compat) ─────────────────────────── @commands.command(name='currency') - async def currency(self, ctx, amount: float = 1, from_cur: str = None, to_cur: str = None): - """Convert *amount* from *from_cur* to *to_cur*. - If *amount* is omitted the command is parsed as ``!currency ``. - """ - # discord.py passes arguments left‑to‑right; we need to support two signatures. - # When the user supplies only two strings, *amount* will be parsed as the first string. + async def currency_prefix(self, ctx, amount: float = 1, from_cur: str = None, to_cur: str = None): + """Convert amount from from_cur to to_cur. Usage: !currency [amount] """ if from_cur is None and to_cur is None: - # only two arguments were given, treat them as from/to and default amount=1 - to_cur = amount # type: ignore + to_cur = amount # type: ignore from_cur = str(amount) # type: ignore amount = 1.0 try: - from_code = self._norm(str(from_cur)) - to_code = self._norm(str(to_cur)) + from_code = _norm(str(from_cur)) + to_code = _norm(str(to_cur)) except ValueError as e: - return await ctx.reply(f"❌ {e}") + return await ctx.reply(f"Unknown currency: {e}") try: - rates = await self._get_rates(from_code) + rates = await _get_rates(from_code) except Exception: - return await ctx.reply('❌ could not fetch conversion rates, try again later') + return await ctx.reply('Could not fetch conversion rates, try again later.') rate = rates.get(to_code) if rate is None: - return await ctx.reply(f"❌ unsupported conversion: {from_code} → {to_code}") - result = amount * rate - embed = discord.Embed(title='Currency conversion', color=0x00ff00) - embed.add_field(name='From', value=f"{amount:g} {from_code}", inline=True) - embed.add_field(name='To', value=f"{result:g} {to_code}", inline=True) - embed.add_field(name='Rate', value=f"1 {from_code} = {rate:g} {to_code}", inline=False) - embed.set_footer(text='Data from frankfurter.app') - await ctx.reply(embed=embed) + return await ctx.reply(f"Unsupported conversion: {from_code} to {to_code}") + await ctx.reply(embed=_make_embed(amount, from_code, to_code, rate)) -async def setup(bot): + +async def setup(bot: commands.Bot): await bot.add_cog(CurrencyConverter(bot)) diff --git a/main.py b/main.py index c0dbbc7..6f995e2 100644 --- a/main.py +++ b/main.py @@ -40,6 +40,12 @@ async def on_ready(): print(f'✅ {bot.user} is online!') asyncio.create_task(forwarder.start()) asyncio.create_task(twitter.start()) + try: + synced = await bot.tree.sync() + print(f'✅ synced {len(synced)} slash command(s)') + except Exception as e: + print(f'slash command sync failed: {e}') + async def main(): async with bot: