From 5a5814bed2b5b247f6681b7bc19a26e70bcb0a5b Mon Sep 17 00:00:00 2001 From: Valerie Date: Sun, 25 May 2025 04:18:39 -0400 Subject: [PATCH] Add API key management and session handling to RubyAPI cog. Implement commands to set the API key and verify users, enhancing interaction with external API. Introduce error handling for API requests and ensure proper session closure on cog unload. --- rubyapi/rubyapi.py | 116 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 8 deletions(-) diff --git a/rubyapi/rubyapi.py b/rubyapi/rubyapi.py index 03b30cf..6a493d1 100644 --- a/rubyapi/rubyapi.py +++ b/rubyapi/rubyapi.py @@ -1,4 +1,5 @@ from typing import Optional +import aiohttp import discord from redbot.core import commands, Config from redbot.core.bot import Red @@ -11,15 +12,21 @@ class RubyAPI(commands.Cog): def __init__(self, bot: Red): self.bot = bot self.config = Config.get_conf(self, identifier=867530999, force_registration=True) + self.session = aiohttp.ClientSession() default_guild = { "interaction_url": "https://ruby.valerie.lol/api/interactions", "verify_url": "https://ruby.valerie.lol/verify-user", - "enabled": False + "enabled": False, + "api_key": None } self.config.register_guild(**default_guild) + def cog_unload(self): + if self.session: + self.bot.loop.create_task(self.session.close()) + @commands.group(name="rubyapi") @commands.admin_or_permissions(manage_guild=True) async def rubyapi(self, ctx: commands.Context): @@ -33,6 +40,16 @@ class RubyAPI(commands.Cog): status = "enabled" if toggle else "disabled" await ctx.send(f"Ruby API integration has been {status}.") + @rubyapi.command(name="setkey") + async def set_api_key(self, ctx: commands.Context, api_key: str): + """Set the API key for authentication.""" + await self.config.guild(ctx.guild).api_key.set(api_key) + await ctx.send("API key has been set. I'll delete your message for security.") + try: + await ctx.message.delete() + except discord.HTTPException: + pass + @rubyapi.command(name="setinteraction") async def set_interaction_url(self, ctx: commands.Context, url: str): """Set the interaction endpoint URL.""" @@ -59,16 +76,35 @@ class RubyAPI(commands.Cog): enabled = "Yes" if guild_config["enabled"] else "No" interaction_url = guild_config["interaction_url"] verify_url = guild_config["verify_url"] + has_api_key = "Yes" if guild_config["api_key"] else "No" message = ( "Ruby API Settings:\n" f"Enabled: {enabled}\n" + f"API Key Set: {has_api_key}\n" f"Interaction URL: {interaction_url}\n" f"Verify URL: {verify_url}" ) await ctx.send(box(message)) + async def _make_api_request(self, url: str, method: str = "GET", **kwargs): + """Make an API request with proper headers and error handling.""" + headers = kwargs.pop("headers", {}) + if api_key := kwargs.pop("api_key", None): + headers["Authorization"] = f"Bearer {api_key}" + + try: + async with self.session.request(method, url, headers=headers, **kwargs) as resp: + if resp.status == 429: # Rate limited + retry_after = float(resp.headers.get("Retry-After", 5)) + return {"error": "rate_limited", "retry_after": retry_after} + + data = await resp.json() + return data + except aiohttp.ClientError as e: + return {"error": f"API request failed: {str(e)}"} + @commands.command() async def verifyuser(self, ctx: commands.Context, user: Optional[discord.Member] = None): """Verify a user's linked roles.""" @@ -77,10 +113,44 @@ class RubyAPI(commands.Cog): user = user or ctx.author verify_url = await self.config.guild(ctx.guild).verify_url() + api_key = await self.config.guild(ctx.guild).api_key() - # Here you would implement the actual API call to verify the user - # For demonstration, we'll just show a message - await ctx.send(f"Verification would be performed for {user.mention} using {verify_url}") + if not api_key: + return await ctx.send("API key has not been set. Please set it using `[p]rubyapi setkey`") + + payload = { + "user_id": str(user.id), + "guild_id": str(ctx.guild.id) + } + + async with ctx.typing(): + result = await self._make_api_request( + verify_url, + method="POST", + json=payload, + api_key=api_key + ) + + if "error" in result: + return await ctx.send(f"Error verifying user: {result['error']}") + + if result.get("verified", False): + roles_to_add = [] + for role_id in result.get("roles", []): + if role := ctx.guild.get_role(int(role_id)): + roles_to_add.append(role) + + if roles_to_add: + try: + await user.add_roles(*roles_to_add, reason="Linked roles verification") + role_names = ", ".join(role.name for role in roles_to_add) + await ctx.send(f"Verified {user.mention}! Added roles: {role_names}") + except discord.HTTPException as e: + await ctx.send(f"Error adding roles: {str(e)}") + else: + await ctx.send(f"Verified {user.mention}, but no roles needed to be added.") + else: + await ctx.send(f"Could not verify {user.mention}. Please make sure their accounts are properly linked.") @commands.Cog.listener() async def on_interaction(self, interaction: discord.Interaction): @@ -90,8 +160,38 @@ class RubyAPI(commands.Cog): if not await self.config.guild(interaction.guild).enabled(): return - - # Here you would implement the actual interaction handling - # For demonstration, we'll just log the interaction + interaction_url = await self.config.guild(interaction.guild).interaction_url() - # You would typically make an API request to the interaction_url here \ No newline at end of file + api_key = await self.config.guild(interaction.guild).api_key() + + if not api_key: + return # Silent return if API key is not set + + # Forward the interaction to the API + payload = { + "type": interaction.type.value, + "guild_id": str(interaction.guild_id), + "channel_id": str(interaction.channel_id), + "data": interaction.data, + "member": { + "user": { + "id": str(interaction.user.id), + "username": interaction.user.name, + "discriminator": interaction.user.discriminator, + }, + "roles": [str(role.id) for role in interaction.user.roles], + } if interaction.user else None + } + + result = await self._make_api_request( + interaction_url, + method="POST", + json=payload, + api_key=api_key + ) + + # Handle the API response if needed + if "error" in result: + # Log the error but don't respond to the interaction + # as it might have been handled elsewhere + print(f"Error forwarding interaction: {result['error']}") \ No newline at end of file