Source code for byte_bot.byte.plugins.admin

"""Plugins for admins.

.. todo:: add an unload cog command.
"""

import discord
import httpx
from discord import Interaction
from discord.app_commands import command as app_command
from discord.ext import commands
from discord.ext.commands import Bot, Cog, Context, command, group, is_owner
from httpx import ConnectError

__all__ = ("AdminCommands", "setup")

from byte_bot.byte.lib import settings
from byte_bot.byte.lib.checks import is_byte_dev
from byte_bot.byte.lib.log import get_logger
from byte_bot.server.lib.settings import ServerSettings

logger = get_logger()
server_settings = ServerSettings()


[docs] class AdminCommands(Cog): """Admin command cog."""
[docs] def __init__(self, bot: Bot) -> None: """Initialize cog.""" self.bot = bot self.__cog_name__ = "Admin Commands" # type: ignore[misc]
@group(name="admin") @is_byte_dev() async def admin(self, ctx: Context) -> None: """Commands for bot admins.""" if ctx.invoked_subcommand is None: await ctx.send("Invalid admin command passed...") await ctx.send_help(ctx.command) @command(name="list-cogs", help="Lists all loaded cogs.", aliases=["lc"], hidden=True) @is_owner() async def list_cogs(self, ctx: Context) -> None: """Lists all loaded cogs. Args: ctx: Context object. """ cogs = [cog.split(".")[-1] for cog in self.bot.extensions] await ctx.send(f"Loaded cogs: {', '.join(cogs)}") @command(name="reload", help="Reloads a cog.", aliases=["rl"], hidden=True) @is_owner() async def reload(self, ctx: Context, cog: str = "all") -> None: """Reloads a cog or all cogs if specified. Args: ctx: Context object. cog: Name of cog to reload. Default is "all". """ if cog.lower() == "all": await self.reload_all_cogs(ctx) else: await self.reload_single_cog(ctx, cog)
[docs] async def reload_all_cogs(self, ctx: Context) -> None: """Reload all cogs. Args: ctx: Context object. """ results = [] for extension in list(self.bot.extensions): cog_name = extension.split(".")[-1] result = await self.reload_single_cog(ctx, cog_name, send_message=False) results.append(result) results.append("All cogs reloaded!") await ctx.send("\n".join(results))
[docs] async def reload_single_cog(self, ctx: Context, cog: str, send_message: bool = True) -> str: """Reload a single cog.""" try: await self.bot.reload_extension(f"plugins.{cog}") message = f"Cog `{cog}` reloaded!" except (commands.ExtensionNotLoaded, commands.ExtensionNotFound) as e: message = f"Error with cog `{cog}`: {e!s}" if send_message: await ctx.send(message) return message
@app_command(name="sync") @is_byte_dev() async def tree_sync(self, interaction: Interaction) -> None: """Slash command to perform a global sync.""" results = await self.bot.tree.sync() await interaction.response.send_message("\n".join(i.name for i in results), ephemeral=True) @command(name="bootstrap-guild", help="Bootstrap existing guild to database (dev only).", hidden=True) @is_byte_dev() async def bootstrap_guild(self, ctx: Context, guild_id: int | None = None) -> None: """Bootstrap an existing guild to the database. Args: ctx: Context object. guild_id: Guild ID to bootstrap. If not provided, uses current guild. """ guild = await self._get_target_guild(ctx, guild_id) if not guild: return await ctx.send(f"🔄 Bootstrapping guild {guild.name} (ID: {guild.id})...") await self._sync_guild_commands(guild) await self._register_guild_in_database(ctx, guild) async def _get_target_guild(self, ctx: Context, guild_id: int | None) -> discord.Guild | None: """Get the target guild for bootstrapping.""" target_guild_id = guild_id or (ctx.guild.id if ctx.guild else None) if not target_guild_id: await ctx.send("❌ No guild ID provided and command not used in a guild.") return None guild = self.bot.get_guild(target_guild_id) if not guild: await ctx.send(f"❌ Bot is not in guild with ID {target_guild_id}") return None return guild async def _sync_guild_commands(self, guild: discord.Guild) -> None: """Sync commands to the guild.""" try: await self.bot.tree.sync(guild=guild) logger.info("Commands synced to guild %s (id: %s)", guild.name, guild.id) except Exception: logger.exception("Failed to sync commands to guild %s", guild.name) async def _register_guild_in_database(self, ctx: Context, guild: discord.Guild) -> None: """Register guild in database via API.""" api_url = f"http://{server_settings.HOST}:{server_settings.PORT}/api/guilds/create?guild_id={guild.id}&guild_name={guild.name}" try: async with httpx.AsyncClient() as client: response = await client.post(api_url) await self._handle_api_response(ctx, guild, response) except ConnectError: error_msg = f"Failed to connect to API to bootstrap guild {guild.name} (id: {guild.id})" logger.exception(error_msg) await ctx.send(f"❌ {error_msg}") async def _handle_api_response(self, ctx: Context, guild: discord.Guild, response: httpx.Response) -> None: """Handle API response for guild registration.""" if response.status_code == httpx.codes.CREATED: await self._send_success_message(ctx, guild) await self._notify_dev_channel(guild) elif response.status_code == httpx.codes.CONFLICT: await ctx.send(f"⚠️ Guild {guild.name} already exists in database") else: error_msg = f"Failed to add guild to database (status: {response.status_code})" logger.error(error_msg) await ctx.send(f"❌ {error_msg}") async def _send_success_message(self, ctx: Context, guild: discord.Guild) -> None: """Send success message to user.""" logger.info("Successfully bootstrapped guild %s (id: %s)", guild.name, guild.id) embed = discord.Embed( title="Guild Bootstrapped", description=f"Successfully bootstrapped guild {guild.name} (ID: {guild.id})", color=discord.Color.green(), ) embed.add_field(name="Commands Synced", value="✅", inline=True) embed.add_field(name="Database Entry", value="✅", inline=True) await ctx.send(embed=embed) async def _notify_dev_channel(self, guild: discord.Guild) -> None: """Notify dev channel about guild bootstrap.""" dev_guild = self.bot.get_guild(settings.discord.DEV_GUILD_ID) if not dev_guild: return dev_channel = dev_guild.get_channel(settings.discord.DEV_GUILD_INTERNAL_ID) if not dev_channel or not hasattr(dev_channel, "send"): return embed = discord.Embed( title="Guild Bootstrapped", description=f"Guild {guild.name} (ID: {guild.id}) was manually bootstrapped", color=discord.Color.blue(), ) await dev_channel.send(embed=embed) # type: ignore[attr-defined]
[docs] async def setup(bot: Bot) -> None: """Add cog to bot. Args: bot: Bot object. """ await bot.add_cog(AdminCommands(bot))