Skip to content

Commit f27f5f5

Browse files
authored
feat: add bootstrapping command (#100)
1 parent fd9b92f commit f27f5f5

File tree

3 files changed

+274
-367
lines changed

3 files changed

+274
-367
lines changed

byte_bot/byte/bot.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from byte_bot.byte.lib import settings
1616
from byte_bot.byte.lib.log import get_logger
17+
from byte_bot.server.lib.settings import ServerSettings
1718

1819
__all__ = [
1920
"Byte",
@@ -22,6 +23,7 @@
2223

2324
logger = get_logger()
2425
load_dotenv()
26+
server_settings = ServerSettings()
2527

2628

2729
class Byte(Bot):
@@ -115,7 +117,7 @@ async def on_guild_join(self, guild: discord.Guild) -> None:
115117
guild: Guild object.
116118
"""
117119
await self.tree.sync(guild=guild)
118-
api_url = f"http://0.0.0.0:8000/api/guilds/create?guild_id={guild.id}&guild_name={guild.name}"
120+
api_url = f"http://{server_settings.HOST}:{server_settings.PORT}/api/guilds/create?guild_id={guild.id}&guild_name={guild.name}"
119121

120122
try:
121123
async with httpx.AsyncClient() as client:

byte_bot/byte/plugins/admin.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,23 @@
33
.. todo:: add an unload cog command.
44
"""
55

6+
import discord
7+
import httpx
68
from discord import Interaction
79
from discord.app_commands import command as app_command
810
from discord.ext import commands
911
from discord.ext.commands import Bot, Cog, Context, command, group, is_owner
12+
from httpx import ConnectError
1013

1114
__all__ = ("AdminCommands", "setup")
1215

1316
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()
1423

1524

1625
class AdminCommands(Cog):
@@ -88,6 +97,101 @@ async def tree_sync(self, interaction: Interaction) -> None:
8897
results = await self.bot.tree.sync()
8998
await interaction.response.send_message("\n".join(i.name for i in results), ephemeral=True)
9099

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+
91195

92196
async def setup(bot: Bot) -> None:
93197
"""Add cog to bot.

0 commit comments

Comments
 (0)