from typing import Optional import aiohttp import discord from redbot.core import commands, Config, checks from redbot.core.bot import Red from redbot.core.utils.chat_formatting import box class RubyAPI(commands.Cog): """Ruby API integration for interactions and linked roles verification.""" def __init__(self, bot: Red): self.bot = bot self.config = Config.get_conf(self, identifier=867530999, force_registration=True) self.session = aiohttp.ClientSession() default_global = { "interaction_url": "https://ruby.valerie.lol/api/verify-user", # Default interaction URL "verify_url": "https://ruby.valerie.lol/api/verify-user", # Default verify URL "enabled": True, # Enabled by default "api_key": None } self.config.register_global(**default_global) def cog_unload(self): if self.session: self.bot.loop.create_task(self.session.close()) async def cog_check(self, ctx: commands.Context) -> bool: """Cog-wide check to ensure only bot team members can use commands.""" return await self.bot.is_owner(ctx.author) or await self.bot.is_co_owner(ctx.author) @commands.group(name="rubyapi") async def rubyapi(self, ctx: commands.Context): """Ruby API configuration commands. Bot team only.""" pass @rubyapi.command(name="enable") async def enable_integration(self, ctx: commands.Context, toggle: bool): """Enable or disable Ruby API integration globally.""" await self.config.enabled.set(toggle) status = "enabled" if toggle else "disabled" await ctx.send(f"Ruby API integration has been {status} globally.") @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.api_key.set(api_key) await ctx.send("API key has been set globally. 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.""" if not url.startswith(("http://", "https://")): return await ctx.send("Please provide a valid URL starting with http:// or https://") await self.config.interaction_url.set(url) await ctx.send("Interaction endpoint URL has been updated globally.") @rubyapi.command(name="setverify") async def set_verify_url(self, ctx: commands.Context, url: str): """Set the verification endpoint URL.""" if not url.startswith(("http://", "https://")): return await ctx.send("Please provide a valid URL starting with http:// or https://") await self.config.verify_url.set(url) await ctx.send("Verification endpoint URL has been updated globally.") @rubyapi.command(name="settings") async def show_settings(self, ctx: commands.Context): """Show current Ruby API settings.""" config = await self.config.all() enabled = "Yes" if config["enabled"] else "No" interaction_url = config["interaction_url"] verify_url = config["verify_url"] has_api_key = "Yes" if config["api_key"] else "No" message = ( "Ruby API Global 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 := await self.config.api_key(): 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. Bot team only.""" if not await self.config.enabled(): return await ctx.send("Ruby API integration is not enabled globally.") user = user or ctx.author verify_url = await self.config.verify_url() api_key = await self.config.api_key() 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 ) 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): """Handle incoming Discord interactions.""" if not await self.config.enabled(): return interaction_url = await self.config.interaction_url() # 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 ) # 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']}")