Skip to content

Commit 361201d

Browse files
authored
Merge pull request #182 from hackthebox/feature/dynamic-roles-db-combined-admin
fix: combine role_admin into admin cog for proper subgroup handling
2 parents 2aff2ef + 03196cd commit 361201d

4 files changed

Lines changed: 424 additions & 394 deletions

File tree

src/cmds/core/admin.py

Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,153 @@
33
import logging
44

55
import discord
6+
from discord import ApplicationContext, Interaction, Option, WebhookMessage
67
from discord.ext import commands
8+
from discord.ext.commands import has_any_role
79

810
from src.bot import Bot
911
from src.core import settings
12+
from src.database.models.dynamic_role import RoleCategory
1013

1114
logger = logging.getLogger(__name__)
1215

13-
# Top-level admin command group
14-
# Subcommands (like 'role') are registered by other cogs using admin.create_subgroup()
15-
admin = discord.SlashCommandGroup(
16-
"admin",
17-
"Bot administration commands",
18-
guild_ids=settings.guild_ids,
19-
)
16+
CATEGORY_CHOICES = [c.value for c in RoleCategory]
2017

2118

2219
class AdminCog(commands.Cog):
23-
"""Admin commands placeholder cog.
24-
25-
This cog doesn't define any commands directly - it just ensures the
26-
/admin group is registered. Subcommands are added by other cogs
27-
via admin.create_subgroup().
28-
"""
20+
"""Admin commands for bot administration."""
2921

3022
def __init__(self, bot: Bot):
3123
self.bot = bot
3224

25+
admin = discord.SlashCommandGroup(
26+
"admin",
27+
"Bot administration commands",
28+
guild_ids=settings.guild_ids,
29+
)
30+
31+
role = admin.create_subgroup(
32+
"role",
33+
"Manage dynamic Discord roles",
34+
)
35+
36+
@role.command(description="Add a new dynamic role.")
37+
@has_any_role(*settings.role_groups.get("ALL_ADMINS"))
38+
async def add(
39+
self,
40+
ctx: ApplicationContext,
41+
category: Option(str, "Role category", choices=CATEGORY_CHOICES),
42+
key: Option(str, "Lookup key (e.g. 'Omniscient', 'CWPE')"),
43+
role: Option(discord.Role, "The Discord role"),
44+
display_name: Option(str, "Human-readable name"),
45+
description: Option(str, "Description (for joinable roles)", required=False),
46+
cert_full_name: Option(str, "Full cert name from HTB platform (academy_cert only)", required=False),
47+
cert_integer_id: Option(int, "Platform cert ID (academy_cert only)", required=False),
48+
) -> Interaction | WebhookMessage:
49+
"""Add a new dynamic role to the database."""
50+
try:
51+
cat = RoleCategory(category)
52+
except ValueError:
53+
return await ctx.respond(f"Invalid category: {category}", ephemeral=True)
54+
55+
await self.bot.role_manager.add_role(
56+
key=key,
57+
category=cat,
58+
discord_role_id=role.id,
59+
display_name=display_name,
60+
description=description,
61+
cert_full_name=cert_full_name,
62+
cert_integer_id=cert_integer_id,
63+
)
64+
return await ctx.respond(
65+
f"Added dynamic role: `{category}/{key}` -> {role.mention}",
66+
ephemeral=True,
67+
)
68+
69+
@role.command(description="Remove a dynamic role.")
70+
@has_any_role(*settings.role_groups.get("ALL_ADMINS"))
71+
async def remove(
72+
self,
73+
ctx: ApplicationContext,
74+
category: Option(str, "Role category", choices=CATEGORY_CHOICES),
75+
key: Option(str, "Lookup key to remove"),
76+
) -> Interaction | WebhookMessage:
77+
"""Remove a dynamic role from the database."""
78+
try:
79+
cat = RoleCategory(category)
80+
except ValueError:
81+
return await ctx.respond(f"Invalid category: {category}", ephemeral=True)
82+
83+
deleted = await self.bot.role_manager.remove_role(cat, key)
84+
if deleted:
85+
return await ctx.respond(f"Removed dynamic role: `{category}/{key}`", ephemeral=True)
86+
return await ctx.respond(f"No role found for `{category}/{key}`", ephemeral=True)
87+
88+
@role.command(description="Update a dynamic role's Discord role.")
89+
@has_any_role(*settings.role_groups.get("ALL_ADMINS"))
90+
async def update(
91+
self,
92+
ctx: ApplicationContext,
93+
category: Option(str, "Role category", choices=CATEGORY_CHOICES),
94+
key: Option(str, "Lookup key to update"),
95+
role: Option(discord.Role, "The new Discord role"),
96+
) -> Interaction | WebhookMessage:
97+
"""Update a dynamic role's Discord ID."""
98+
try:
99+
cat = RoleCategory(category)
100+
except ValueError:
101+
return await ctx.respond(f"Invalid category: {category}", ephemeral=True)
102+
103+
updated = await self.bot.role_manager.update_role(cat, key, role.id)
104+
if updated:
105+
return await ctx.respond(
106+
f"Updated `{category}/{key}` -> {role.mention}",
107+
ephemeral=True,
108+
)
109+
return await ctx.respond(f"No role found for `{category}/{key}`", ephemeral=True)
110+
111+
@role.command(description="List configured dynamic roles.")
112+
@has_any_role(*settings.role_groups.get("ALL_ADMINS"), *settings.role_groups.get("ALL_MODS"))
113+
async def list(
114+
self,
115+
ctx: ApplicationContext,
116+
category: Option(str, "Filter by category", choices=CATEGORY_CHOICES, required=False),
117+
) -> Interaction | WebhookMessage:
118+
"""List all configured dynamic roles."""
119+
cat = RoleCategory(category) if category else None
120+
roles = await self.bot.role_manager.list_roles(cat)
121+
122+
if not roles:
123+
return await ctx.respond("No dynamic roles configured.", ephemeral=True)
124+
125+
# Group by category for display
126+
grouped: dict[str, list[str]] = {}
127+
for r in roles:
128+
cat_name = r.category.value
129+
if cat_name not in grouped:
130+
grouped[cat_name] = []
131+
guild_role = ctx.guild.get_role(r.discord_role_id)
132+
role_mention = guild_role.mention if guild_role else f"`{r.discord_role_id}`"
133+
grouped[cat_name].append(f"`{r.key}` -> {role_mention} ({r.display_name})")
134+
135+
embed = discord.Embed(title="Dynamic Roles", color=0x9ACC14)
136+
for cat_name, entries in grouped.items():
137+
embed.add_field(
138+
name=cat_name,
139+
value="\n".join(entries[:10]) + (f"\n... and {len(entries) - 10} more" if len(entries) > 10 else ""),
140+
inline=False,
141+
)
142+
143+
return await ctx.respond(embed=embed, ephemeral=True)
144+
145+
@role.command(description="Force reload dynamic roles from database.")
146+
@has_any_role(*settings.role_groups.get("ALL_ADMINS"))
147+
async def reload(self, ctx: ApplicationContext) -> Interaction | WebhookMessage:
148+
"""Force reload the role manager cache from the database."""
149+
await self.bot.role_manager.reload()
150+
return await ctx.respond("Dynamic roles reloaded from database.", ephemeral=True)
151+
33152

34153
def setup(bot: Bot) -> None:
35-
"""Load the AdminCog and register the admin command group."""
36-
cog = AdminCog(bot)
37-
bot.add_cog(cog)
38-
# Register the admin group at module level so other cogs can add subgroups
39-
bot.add_application_command(admin)
154+
"""Load the AdminCog."""
155+
bot.add_cog(AdminCog(bot))

src/cmds/core/role_admin.py

Lines changed: 0 additions & 149 deletions
This file was deleted.

0 commit comments

Comments
 (0)