9c700b8
commit 9c700b8
7 files changed
+317 -4
Showing 7 changed files+317-4
▼apps/bot/src/commands/index.ts
+2-0
| @@ -11,6 +11,7 @@ import { investigateCommand } from "./investigate.js"; | ||
11 | 11 | import { prCommand } from "./pr.js"; |
12 | 12 | import { releaseCommand } from "./release.js"; |
13 | 13 | import { repoCommand } from "./repo.js"; |
14 | +import { roleCommand } from "./role.js"; | |
14 | 15 | import { searchCommand } from "./search.js"; |
15 | 16 | import { securityCommand } from "./security.js"; |
16 | 17 | import { settingsCommand } from "./settings.js"; |
| @@ -42,6 +43,7 @@ export const commands: Command[] = [ | ||
42 | 43 | askCommand, |
43 | 44 | toolsCommand, |
44 | 45 | settingsCommand, |
46 | + roleCommand, | |
45 | 47 | helpCommand, |
46 | 48 | ]; |
47 | 49 | |
48 | 50 | |
▼apps/bot/src/commands/role.ts
+300-0
| @@ -0,0 +1,300 @@ | ||
1 | +import { db, auditLogs, servers, serverMemberships, users } from "@devpulse/database"; | |
2 | +import { getUserRole, requirePermission } from "@devpulse/permissions"; | |
3 | +import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js"; | |
4 | +import { and, eq } from "drizzle-orm"; | |
5 | +import { BrandColors } from "../ui/colors.js"; | |
6 | +import { createBaseEmbed, createErrorEmbed } from "../ui/embeds.js"; | |
7 | +import { NF } from "../ui/icons.js"; | |
8 | +import type { Command } from "./types.js"; | |
9 | + | |
10 | +type AppRole = "admin" | "lead" | "developer" | "viewer"; | |
11 | + | |
12 | +export const roleCommand: Command = { | |
13 | + data: new SlashCommandBuilder() | |
14 | + .setName("role") | |
15 | + .setDescription("Manage team roles: assign, list, and remove developer permissions") | |
16 | + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) | |
17 | + .addSubcommand((sub) => | |
18 | + sub | |
19 | + .setName("assign") | |
20 | + .setDescription("Assign a role to a team member") | |
21 | + .addUserOption((opt) => | |
22 | + opt.setName("user").setDescription("Discord user to assign role to").setRequired(true), | |
23 | + ) | |
24 | + .addStringOption((opt) => | |
25 | + opt | |
26 | + .setName("role") | |
27 | + .setDescription("Role to assign") | |
28 | + .addChoices( | |
29 | + { name: "Admin (Full access)", value: "admin" }, | |
30 | + { name: "Lead (PR merge, review)", value: "lead" }, | |
31 | + { name: "Developer (Standard access)", value: "developer" }, | |
32 | + { name: "Viewer (Read-only)", value: "viewer" }, | |
33 | + ) | |
34 | + .setRequired(true), | |
35 | + ), | |
36 | + ) | |
37 | + .addSubcommand((sub) => | |
38 | + sub.setName("list").setDescription("List all role assignments in this server"), | |
39 | + ) | |
40 | + .addSubcommand((sub) => | |
41 | + sub | |
42 | + .setName("remove") | |
43 | + .setDescription("Remove a user's role assignment (reset to viewer)") | |
44 | + .addUserOption((opt) => | |
45 | + opt.setName("user").setDescription("Discord user to remove role from").setRequired(true), | |
46 | + ), | |
47 | + ), | |
48 | + | |
49 | + async execute(interaction) { | |
50 | + if (!interaction.guildId) { | |
51 | + await interaction.reply({ | |
52 | + content: `${NF.cross} This command can only be used in a Discord server.`, | |
53 | + ephemeral: true, | |
54 | + }); | |
55 | + return; | |
56 | + } | |
57 | + | |
58 | + const subcommand = interaction.options.getSubcommand(); | |
59 | + const guildId = interaction.guildId; | |
60 | + | |
61 | + try { | |
62 | + if (subcommand === "list") { | |
63 | + await handleRoleList(interaction, guildId); | |
64 | + return; | |
65 | + } | |
66 | + | |
67 | + // Assign and Remove require admin role | |
68 | + await requirePermission(guildId, interaction.user.id, "admin"); | |
69 | + | |
70 | + if (subcommand === "assign") { | |
71 | + await handleRoleAssign(interaction, guildId); | |
72 | + return; | |
73 | + } | |
74 | + | |
75 | + if (subcommand === "remove") { | |
76 | + await handleRoleRemove(interaction, guildId); | |
77 | + return; | |
78 | + } | |
79 | + } catch (err: unknown) { | |
80 | + const errorEmbed = createErrorEmbed(err instanceof Error ? err : new Error(String(err))); | |
81 | + if (interaction.replied || interaction.deferred) { | |
82 | + await interaction.editReply({ embeds: [errorEmbed] }).catch(() => {}); | |
83 | + } else { | |
84 | + await interaction.reply({ embeds: [errorEmbed], ephemeral: true }).catch(() => {}); | |
85 | + } | |
86 | + } | |
87 | + }, | |
88 | +}; | |
89 | + | |
90 | +const ROLE_HIERARCHY: Record<string, number> = { admin: 40, lead: 30, developer: 20, viewer: 10 }; | |
91 | +const ROLE_ICONS: Record<string, string> = { | |
92 | + admin: "👑", | |
93 | + lead: "⭐", | |
94 | + developer: " | |