Files
aurorabot/bot/commands/admin/featureflags.ts
syntaxbullet a5e3534260 feat(commands): add /featureflags admin command
Add comprehensive feature flag management with subcommands:
- list: Show all feature flags
- create/delete: Manage flags
- enable/disable: Toggle flags
- grant/revoke: Manage access for users/roles/guilds
- access: View access records for a flag
2026-02-12 14:50:36 +01:00

298 lines
11 KiB
TypeScript

import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, Colors, userMention, roleMention, ChatInputCommandInteraction } from "discord.js";
import { featureFlagsService } from "@shared/modules/feature-flags/feature-flags.service";
import { createBaseEmbed, createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
import { UserError } from "@shared/lib/errors";
export const featureflags = createCommand({
data: new SlashCommandBuilder()
.setName("featureflags")
.setDescription("Manage feature flags for beta testing")
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(sub =>
sub.setName("list")
.setDescription("List all feature flags")
)
.addSubcommand(sub =>
sub.setName("create")
.setDescription("Create a new feature flag")
.addStringOption(opt =>
opt.setName("name")
.setDescription("Name of the feature flag")
.setRequired(true)
)
.addStringOption(opt =>
opt.setName("description")
.setDescription("Description of the feature flag")
.setRequired(false)
)
)
.addSubcommand(sub =>
sub.setName("delete")
.setDescription("Delete a feature flag")
.addStringOption(opt =>
opt.setName("name")
.setDescription("Name of the feature flag")
.setRequired(true)
.setAutocomplete(true)
)
)
.addSubcommand(sub =>
sub.setName("enable")
.setDescription("Enable a feature flag")
.addStringOption(opt =>
opt.setName("name")
.setDescription("Name of the feature flag")
.setRequired(true)
.setAutocomplete(true)
)
)
.addSubcommand(sub =>
sub.setName("disable")
.setDescription("Disable a feature flag")
.addStringOption(opt =>
opt.setName("name")
.setDescription("Name of the feature flag")
.setRequired(true)
.setAutocomplete(true)
)
)
.addSubcommand(sub =>
sub.setName("grant")
.setDescription("Grant access to a feature flag")
.addStringOption(opt =>
opt.setName("name")
.setDescription("Name of the feature flag")
.setRequired(true)
.setAutocomplete(true)
)
.addUserOption(opt =>
opt.setName("user")
.setDescription("User to grant access to")
.setRequired(false)
)
.addRoleOption(opt =>
opt.setName("role")
.setDescription("Role to grant access to")
.setRequired(false)
)
)
.addSubcommand(sub =>
sub.setName("revoke")
.setDescription("Revoke access from a feature flag")
.addIntegerOption(opt =>
opt.setName("id")
.setDescription("Access record ID to revoke")
.setRequired(true)
)
)
.addSubcommand(sub =>
sub.setName("access")
.setDescription("List access records for a feature flag")
.addStringOption(opt =>
opt.setName("name")
.setDescription("Name of the feature flag")
.setRequired(true)
.setAutocomplete(true)
)
),
autocomplete: async (interaction) => {
const focused = interaction.options.getFocused(true);
if (focused.name === "name") {
const flags = await featureFlagsService.listFlags();
const filtered = flags
.filter(f => f.name.toLowerCase().includes(focused.value.toLowerCase()))
.slice(0, 25);
await interaction.respond(
filtered.map(f => ({ name: `${f.name} (${f.enabled ? "enabled" : "disabled"})`, value: f.name }))
);
}
},
execute: async (interaction) => {
await interaction.deferReply();
const subcommand = interaction.options.getSubcommand();
try {
switch (subcommand) {
case "list":
await handleList(interaction);
break;
case "create":
await handleCreate(interaction);
break;
case "delete":
await handleDelete(interaction);
break;
case "enable":
await handleEnable(interaction);
break;
case "disable":
await handleDisable(interaction);
break;
case "grant":
await handleGrant(interaction);
break;
case "revoke":
await handleRevoke(interaction);
break;
case "access":
await handleAccess(interaction);
break;
}
} catch (error) {
if (error instanceof UserError) {
await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
} else {
throw error;
}
}
},
});
async function handleList(interaction: ChatInputCommandInteraction) {
const flags = await featureFlagsService.listFlags();
if (flags.length === 0) {
await interaction.editReply({ embeds: [createBaseEmbed("Feature Flags", "No feature flags have been created yet.", Colors.Blue)] });
return;
}
const embed = createBaseEmbed("Feature Flags", undefined, Colors.Blue)
.addFields(
flags.map(f => ({
name: f.name,
value: `${f.enabled ? "✅ Enabled" : "❌ Disabled"}\n${f.description || "*No description*"}`,
inline: false,
}))
);
await interaction.editReply({ embeds: [embed] });
}
async function handleCreate(interaction: ChatInputCommandInteraction) {
const name = interaction.options.getString("name", true);
const description = interaction.options.getString("description");
const flag = await featureFlagsService.createFlag(name, description ?? undefined);
if (!flag) {
await interaction.editReply({ embeds: [createErrorEmbed("Failed to create feature flag.")] });
return;
}
await interaction.editReply({
embeds: [createSuccessEmbed(`Feature flag "**${flag.name}**" created successfully. Use \`/featureflags enable\` to enable it.`)]
});
}
async function handleDelete(interaction: ChatInputCommandInteraction) {
const name = interaction.options.getString("name", true);
const flag = await featureFlagsService.deleteFlag(name);
await interaction.editReply({
embeds: [createSuccessEmbed(`Feature flag "**${flag.name}**" has been deleted.`)]
});
}
async function handleEnable(interaction: ChatInputCommandInteraction) {
const name = interaction.options.getString("name", true);
const flag = await featureFlagsService.setFlagEnabled(name, true);
await interaction.editReply({
embeds: [createSuccessEmbed(`Feature flag "**${flag.name}**" has been enabled.`)]
});
}
async function handleDisable(interaction: ChatInputCommandInteraction) {
const name = interaction.options.getString("name", true);
const flag = await featureFlagsService.setFlagEnabled(name, false);
await interaction.editReply({
embeds: [createSuccessEmbed(`Feature flag "**${flag.name}**" has been disabled.`)]
});
}
async function handleGrant(interaction: ChatInputCommandInteraction) {
const name = interaction.options.getString("name", true);
const user = interaction.options.getUser("user");
const role = interaction.options.getRole("role");
if (!user && !role) {
await interaction.editReply({
embeds: [createErrorEmbed("You must specify either a user or a role to grant access to.")]
});
return;
}
const access = await featureFlagsService.grantAccess(name, {
userId: user?.id,
roleId: role?.id,
guildId: interaction.guildId!,
});
if (!access) {
await interaction.editReply({ embeds: [createErrorEmbed("Failed to grant access.")] });
return;
}
let target: string;
if (user) {
target = userMention(user.id);
} else if (role) {
target = roleMention(role.id);
} else {
target = "Unknown";
}
await interaction.editReply({
embeds: [createSuccessEmbed(`Access to "**${name}**" granted to ${target} (ID: ${access.id})`)]
});
}
async function handleRevoke(interaction: ChatInputCommandInteraction) {
const id = interaction.options.getInteger("id", true);
const access = await featureFlagsService.revokeAccess(id);
await interaction.editReply({
embeds: [createSuccessEmbed(`Access record #${access.id} has been revoked.`)]
});
}
async function handleAccess(interaction: ChatInputCommandInteraction) {
const name = interaction.options.getString("name", true);
const accessRecords = await featureFlagsService.listAccess(name);
if (accessRecords.length === 0) {
await interaction.editReply({
embeds: [createBaseEmbed("Feature Flag Access", `No access records for "**${name}**".`, Colors.Blue)]
});
return;
}
const fields = accessRecords.map(a => {
let target = "Unknown";
if (a.userId) target = `User: ${userMention(a.userId.toString())}`;
else if (a.roleId) target = `Role: ${roleMention(a.roleId.toString())}`;
else if (a.guildId) target = `Guild: ${a.guildId.toString()}`;
return {
name: `ID: ${a.id}`,
value: target,
inline: true,
};
});
const embed = createBaseEmbed(`Feature Flag Access: ${name}`, undefined, Colors.Blue)
.addFields(fields);
await interaction.editReply({ embeds: [embed] });
}