|
3 | 3 | import logging |
4 | 4 |
|
5 | 5 | import discord |
| 6 | +from discord import ApplicationContext, Interaction, Option, WebhookMessage |
6 | 7 | from discord.ext import commands |
| 8 | +from discord.ext.commands import has_any_role |
7 | 9 |
|
8 | 10 | from src.bot import Bot |
9 | 11 | from src.core import settings |
| 12 | +from src.database.models.dynamic_role import RoleCategory |
10 | 13 |
|
11 | 14 | logger = logging.getLogger(__name__) |
12 | 15 |
|
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] |
20 | 17 |
|
21 | 18 |
|
22 | 19 | 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.""" |
29 | 21 |
|
30 | 22 | def __init__(self, bot: Bot): |
31 | 23 | self.bot = bot |
32 | 24 |
|
| 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 | + |
33 | 152 |
|
34 | 153 | 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)) |
0 commit comments