|
3 | 3 | .. todo:: add an unload cog command. |
4 | 4 | """ |
5 | 5 |
|
| 6 | +import discord |
| 7 | +import httpx |
6 | 8 | from discord import Interaction |
7 | 9 | from discord.app_commands import command as app_command |
8 | 10 | from discord.ext import commands |
9 | 11 | from discord.ext.commands import Bot, Cog, Context, command, group, is_owner |
| 12 | +from httpx import ConnectError |
10 | 13 |
|
11 | 14 | __all__ = ("AdminCommands", "setup") |
12 | 15 |
|
13 | 16 | from byte_bot.byte.lib.checks import is_byte_dev |
| 17 | +from byte_bot.byte.lib import settings |
| 18 | +from byte_bot.byte.lib.log import get_logger |
| 19 | +from byte_bot.server.lib.settings import ServerSettings |
| 20 | + |
| 21 | +logger = get_logger() |
| 22 | +server_settings = ServerSettings() |
14 | 23 |
|
15 | 24 |
|
16 | 25 | class AdminCommands(Cog): |
@@ -88,6 +97,101 @@ async def tree_sync(self, interaction: Interaction) -> None: |
88 | 97 | results = await self.bot.tree.sync() |
89 | 98 | await interaction.response.send_message("\n".join(i.name for i in results), ephemeral=True) |
90 | 99 |
|
| 100 | + @command(name="bootstrap-guild", help="Bootstrap existing guild to database (dev only).", hidden=True) |
| 101 | + @is_byte_dev() |
| 102 | + async def bootstrap_guild(self, ctx: Context, guild_id: int | None = None) -> None: |
| 103 | + """Bootstrap an existing guild to the database. |
| 104 | + |
| 105 | + Args: |
| 106 | + ctx: Context object. |
| 107 | + guild_id: Guild ID to bootstrap. If not provided, uses current guild. |
| 108 | + """ |
| 109 | + guild = await self._get_target_guild(ctx, guild_id) |
| 110 | + if not guild: |
| 111 | + return |
| 112 | + |
| 113 | + await ctx.send(f"🔄 Bootstrapping guild {guild.name} (ID: {guild.id})...") |
| 114 | + |
| 115 | + await self._sync_guild_commands(guild) |
| 116 | + await self._register_guild_in_database(ctx, guild) |
| 117 | + |
| 118 | + async def _get_target_guild(self, ctx: Context, guild_id: int | None) -> discord.Guild | None: |
| 119 | + """Get the target guild for bootstrapping.""" |
| 120 | + target_guild_id = guild_id or (ctx.guild.id if ctx.guild else None) |
| 121 | + |
| 122 | + if not target_guild_id: |
| 123 | + await ctx.send("❌ No guild ID provided and command not used in a guild.") |
| 124 | + return None |
| 125 | + |
| 126 | + guild = self.bot.get_guild(target_guild_id) |
| 127 | + if not guild: |
| 128 | + await ctx.send(f"❌ Bot is not in guild with ID {target_guild_id}") |
| 129 | + return None |
| 130 | + |
| 131 | + return guild |
| 132 | + |
| 133 | + async def _sync_guild_commands(self, guild: discord.Guild) -> None: |
| 134 | + """Sync commands to the guild.""" |
| 135 | + try: |
| 136 | + await self.bot.tree.sync(guild=guild) |
| 137 | + logger.info("Commands synced to guild %s (id: %s)", guild.name, guild.id) |
| 138 | + except Exception as e: |
| 139 | + logger.error("Failed to sync commands to guild %s: %s", guild.name, e) |
| 140 | + |
| 141 | + async def _register_guild_in_database(self, ctx: Context, guild: discord.Guild) -> None: |
| 142 | + """Register guild in database via API.""" |
| 143 | + api_url = f"http://{server_settings.HOST}:{server_settings.PORT}/api/guilds/create?guild_id={guild.id}&guild_name={guild.name}" |
| 144 | + |
| 145 | + try: |
| 146 | + async with httpx.AsyncClient() as client: |
| 147 | + response = await client.post(api_url) |
| 148 | + await self._handle_api_response(ctx, guild, response) |
| 149 | + except ConnectError: |
| 150 | + error_msg = f"Failed to connect to API to bootstrap guild {guild.name} (id: {guild.id})" |
| 151 | + logger.exception(error_msg) |
| 152 | + await ctx.send(f"❌ {error_msg}") |
| 153 | + |
| 154 | + async def _handle_api_response(self, ctx: Context, guild: discord.Guild, response: httpx.Response) -> None: |
| 155 | + """Handle API response for guild registration.""" |
| 156 | + if response.status_code == httpx.codes.CREATED: |
| 157 | + await self._send_success_message(ctx, guild) |
| 158 | + await self._notify_dev_channel(guild) |
| 159 | + elif response.status_code == httpx.codes.CONFLICT: |
| 160 | + await ctx.send(f"⚠️ Guild {guild.name} already exists in database") |
| 161 | + else: |
| 162 | + error_msg = f"Failed to add guild to database (status: {response.status_code})" |
| 163 | + logger.error(error_msg) |
| 164 | + await ctx.send(f"❌ {error_msg}") |
| 165 | + |
| 166 | + async def _send_success_message(self, ctx: Context, guild: discord.Guild) -> None: |
| 167 | + """Send success message to user.""" |
| 168 | + logger.info("Successfully bootstrapped guild %s (id: %s)", guild.name, guild.id) |
| 169 | + embed = discord.Embed( |
| 170 | + title="Guild Bootstrapped", |
| 171 | + description=f"Successfully bootstrapped guild {guild.name} (ID: {guild.id})", |
| 172 | + color=discord.Color.green(), |
| 173 | + ) |
| 174 | + embed.add_field(name="Commands Synced", value="✅", inline=True) |
| 175 | + embed.add_field(name="Database Entry", value="✅", inline=True) |
| 176 | + await ctx.send(embed=embed) |
| 177 | + |
| 178 | + async def _notify_dev_channel(self, guild: discord.Guild) -> None: |
| 179 | + """Notify dev channel about guild bootstrap.""" |
| 180 | + dev_guild = self.bot.get_guild(settings.discord.DEV_GUILD_ID) |
| 181 | + if not dev_guild: |
| 182 | + return |
| 183 | + |
| 184 | + dev_channel = dev_guild.get_channel(settings.discord.DEV_GUILD_INTERNAL_ID) |
| 185 | + if not dev_channel or not hasattr(dev_channel, "send"): |
| 186 | + return |
| 187 | + |
| 188 | + embed = discord.Embed( |
| 189 | + title="Guild Bootstrapped", |
| 190 | + description=f"Guild {guild.name} (ID: {guild.id}) was manually bootstrapped", |
| 191 | + color=discord.Color.blue(), |
| 192 | + ) |
| 193 | + await dev_channel.send(embed=embed) # type: ignore[attr-defined] |
| 194 | + |
91 | 195 |
|
92 | 196 | async def setup(bot: Bot) -> None: |
93 | 197 | """Add cog to bot. |
|
0 commit comments