diff --git a/scripts/audit-discord-roles.ts b/scripts/audit-discord-roles.ts deleted file mode 100644 index c4115c0..0000000 --- a/scripts/audit-discord-roles.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Script to audit Discord roles against the repo configuration. - * - * For each user defined in the repo with a Discord ID, this script: - * 1. Gets their actual Discord roles - * 2. Compares against expected roles based on their memberOf - * 3. Reports discrepancies - * - * Run with: npx ts-node scripts/audit-discord-roles.ts - */ - -import { MEMBERS } from '../src/config/users'; -import { ROLES, getRole } from '../src/config/roles'; -import type { RoleId } from '../src/config/roleIds'; - -const BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || ''; -const GUILD_ID = process.env.DISCORD_GUILD_ID || ''; - -interface DiscordRole { - id: string; - name: string; -} - -interface DiscordMember { - user: { id: string; username: string; global_name: string }; - roles: string[]; -} - -// Build role name -> id lookup from Discord -const discordRoleNameToId = new Map(); -const discordRoleIdToName = new Map(); - -// Build expected role name -> RoleId lookup from config -const syncedRoleNameToRoleId = new Map(); - -async function fetchJson(url: string): Promise { - const response = await fetch(url, { - headers: { Authorization: `Bot ${BOT_TOKEN}` }, - }); - if (!response.ok) { - const error = await response.text(); - throw new Error(`Discord API error: ${response.status} ${error}`); - } - return response.json(); -} - -async function getDiscordRoles(): Promise { - return fetchJson(`https://discord.com/api/v10/guilds/${GUILD_ID}/roles`); -} - -async function getDiscordMember(discordId: string): Promise { - try { - return await fetchJson(`https://discord.com/api/v10/guilds/${GUILD_ID}/members/${discordId}`); - } catch (e) { - return null; - } -} - -/** - * Expand role IDs to include all Discord-implied roles - * (GitHub parent traversal + discordImplies) - */ -function expandDiscordRoles(roleIds: readonly RoleId[]): Set { - const expanded = new Set(); - const queue = [...roleIds]; - - while (queue.length > 0) { - const roleId = queue.shift()!; - if (expanded.has(roleId)) continue; - - const role = getRole(roleId); - if (!role) continue; - - expanded.add(roleId); - - // Add parent role (GitHub hierarchy) - if (role.github?.parent) { - queue.push(role.github.parent); - } - - // Add discord-implied roles - if (role.discordImplies) { - queue.push(...role.discordImplies); - } - } - - return expanded; -} - -async function main() { - if (!BOT_TOKEN || !GUILD_ID) { - console.error('Set DISCORD_BOT_TOKEN and DISCORD_GUILD_ID environment variables'); - process.exit(1); - } - - console.log('Fetching Discord roles...'); - const discordRoles = await getDiscordRoles(); - - // Build role lookups - for (const role of discordRoles) { - discordRoleNameToId.set(role.name, role.id); - discordRoleIdToName.set(role.id, role.name); - } - - // Build synced role name -> RoleId lookup - for (const role of ROLES) { - if (role.discord?.role) { - syncedRoleNameToRoleId.set(role.discord.role, role.id); - } - } - - // Map manual role names to their synced equivalents - const manualToSynced = new Map(); - for (const role of discordRoles) { - const syncedName = `${role.name} (synced)`; - if (discordRoleNameToId.has(syncedName)) { - manualToSynced.set(role.name, syncedName); - } - } - - console.log('\n=== Discord Roles Summary ==='); - console.log(`Total roles: ${discordRoles.length}`); - console.log(`Synced roles: ${discordRoles.filter(r => r.name.endsWith('(synced)')).length}`); - console.log(`Manual roles with synced equivalent: ${manualToSynced.size}`); - - console.log('\n=== Auditing Users ===\n'); - - const issues: string[] = []; - const membersWithDiscord = MEMBERS.filter(m => m.discord); - - for (const member of membersWithDiscord) { - const discordMember = await getDiscordMember(member.discord!); - if (!discordMember) { - issues.push(`āŒ ${member.github || member.email}: Discord ID ${member.discord} not found in guild`); - continue; - } - - const username = discordMember.user.global_name || discordMember.user.username; - - // Get actual role names - const actualRoleNames = new Set( - discordMember.roles.map(id => discordRoleIdToName.get(id) || id) - ); - - // Get expected synced role names based on memberOf - const expandedRoleIds = expandDiscordRoles(member.memberOf); - const expectedSyncedRoles = new Set(); - const expectedManualRoles = new Set(); - - for (const roleId of expandedRoleIds) { - const role = getRole(roleId); - if (role?.discord?.role) { - expectedSyncedRoles.add(role.discord.role); - // Also expect the manual equivalent if it exists - const manualName = role.discord.role.replace(' (synced)', ''); - if (discordRoleNameToId.has(manualName)) { - expectedManualRoles.add(manualName); - } - } - } - - // Check for missing synced roles - const missingSyncedRoles: string[] = []; - for (const expectedRole of expectedSyncedRoles) { - if (!actualRoleNames.has(expectedRole)) { - missingSyncedRoles.push(expectedRole); - } - } - - // Check for manual roles without synced equivalent - const manualWithoutSynced: string[] = []; - for (const roleName of actualRoleNames) { - if (roleName.endsWith('(synced)')) continue; - if (roleName === '@everyone') continue; - - const syncedEquivalent = `${roleName} (synced)`; - if (discordRoleNameToId.has(syncedEquivalent)) { - // This is a manual role that has a synced equivalent - if (!actualRoleNames.has(syncedEquivalent)) { - manualWithoutSynced.push(`${roleName} (has no ${syncedEquivalent})`); - } - } - } - - if (missingSyncedRoles.length > 0 || manualWithoutSynced.length > 0) { - console.log(`\nšŸ‘¤ ${username} (${member.github || member.email})`); - console.log(` Discord ID: ${member.discord}`); - - if (missingSyncedRoles.length > 0) { - console.log(` āŒ Missing synced roles:`); - for (const role of missingSyncedRoles) { - console.log(` - ${role}`); - issues.push(`${username}: Missing ${role}`); - } - } - - if (manualWithoutSynced.length > 0) { - console.log(` āš ļø Manual roles without synced equivalent:`); - for (const role of manualWithoutSynced) { - console.log(` - ${role}`); - issues.push(`${username}: ${role}`); - } - } - } else { - console.log(`āœ… ${username} (${member.github || member.email})`); - } - } - - console.log('\n\n=== Summary ==='); - if (issues.length === 0) { - console.log('āœ… All users have matching roles!'); - } else { - console.log(`āŒ Found ${issues.length} issues:\n`); - for (const issue of issues) { - console.log(` - ${issue}`); - } - } -} - -main().catch(console.error);