From e99ef3b9424cd6b23cb3b686b97d5a3962f3c070 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 15:15:20 +0000 Subject: [PATCH] feat: Add Discord audit routes to Arbiter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New endpoints for Trinity Console: - GET /admin/discord/audit — Full server audit (channels, roles, structure) - GET /admin/discord/channels — Just channels - GET /admin/discord/roles — Just roles Returns: - Server info (name, member count, features) - Categories with nested children - Orphan channels (not in categories) - Role hierarchy with positions and member counts - Permission overwrites per channel Uses existing Discord.js client from app.locals. Chronicler #70 --- .../src/routes/admin/discord-audit.js | 183 ++++++++++++++++++ .../arbiter-3.0/src/routes/admin/index.js | 2 + 2 files changed, 185 insertions(+) create mode 100644 services/arbiter-3.0/src/routes/admin/discord-audit.js diff --git a/services/arbiter-3.0/src/routes/admin/discord-audit.js b/services/arbiter-3.0/src/routes/admin/discord-audit.js new file mode 100644 index 0000000..86c8c71 --- /dev/null +++ b/services/arbiter-3.0/src/routes/admin/discord-audit.js @@ -0,0 +1,183 @@ +/** + * Discord Audit Routes + * Provides server structure auditing via Trinity Console + * + * Created: April 8, 2026 + * Chronicler: #70 + */ + +const express = require('express'); +const router = express.Router(); +const { isAuthenticated, isAdmin } = require('../../middleware/auth'); + +/** + * GET /admin/discord/audit + * Full Discord server audit - channels, roles, members + */ +router.get('/audit', isAuthenticated, isAdmin, async (req, res) => { + try { + const client = req.app.locals.client; + const guildId = process.env.GUILD_ID; + + if (!client || !client.isReady()) { + return res.status(503).json({ error: 'Discord client not ready' }); + } + + const guild = client.guilds.cache.get(guildId); + if (!guild) { + return res.status(404).json({ error: 'Guild not found' }); + } + + // Fetch fresh data + await guild.channels.fetch(); + await guild.roles.fetch(); + + // Build channel structure + const channels = guild.channels.cache.map(ch => ({ + id: ch.id, + name: ch.name, + type: ch.type, + typeName: getChannelTypeName(ch.type), + parentId: ch.parentId, + position: ch.position, + nsfw: ch.nsfw || false, + topic: ch.topic || null, + permissionOverwrites: ch.permissionOverwrites?.cache.map(p => ({ + id: p.id, + type: p.type, // 0 = role, 1 = member + allow: p.allow.bitfield.toString(), + deny: p.deny.bitfield.toString() + })) || [] + })).sort((a, b) => a.position - b.position); + + // Build role structure + const roles = guild.roles.cache.map(r => ({ + id: r.id, + name: r.name, + color: r.hexColor, + position: r.position, + permissions: r.permissions.bitfield.toString(), + mentionable: r.mentionable, + managed: r.managed, // Bot roles + memberCount: r.members.size + })).sort((a, b) => b.position - a.position); // Higher position first + + // Categories with their children + const categories = channels + .filter(ch => ch.type === 4) + .map(cat => ({ + ...cat, + children: channels.filter(ch => ch.parentId === cat.id) + })); + + // Orphan channels (not in any category) + const orphanChannels = channels.filter(ch => !ch.parentId && ch.type !== 4); + + // Server info + const serverInfo = { + id: guild.id, + name: guild.name, + memberCount: guild.memberCount, + ownerId: guild.ownerId, + createdAt: guild.createdAt, + icon: guild.iconURL(), + features: guild.features + }; + + res.json({ + server: serverInfo, + categories, + orphanChannels, + allChannels: channels, + roles, + summary: { + totalChannels: channels.length, + totalRoles: roles.length, + categoryCount: categories.length, + orphanCount: orphanChannels.length + } + }); + + } catch (error) { + console.error('Discord audit error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * GET /admin/discord/channels + * Just channels + */ +router.get('/channels', isAuthenticated, isAdmin, async (req, res) => { + try { + const client = req.app.locals.client; + const guild = client.guilds.cache.get(process.env.GUILD_ID); + + if (!guild) return res.status(404).json({ error: 'Guild not found' }); + + await guild.channels.fetch(); + + const channels = guild.channels.cache.map(ch => ({ + id: ch.id, + name: ch.name, + type: ch.type, + typeName: getChannelTypeName(ch.type), + parentId: ch.parentId, + position: ch.position + })).sort((a, b) => a.position - b.position); + + res.json({ channels }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +/** + * GET /admin/discord/roles + * Just roles + */ +router.get('/roles', isAuthenticated, isAdmin, async (req, res) => { + try { + const client = req.app.locals.client; + const guild = client.guilds.cache.get(process.env.GUILD_ID); + + if (!guild) return res.status(404).json({ error: 'Guild not found' }); + + await guild.roles.fetch(); + + const roles = guild.roles.cache.map(r => ({ + id: r.id, + name: r.name, + color: r.hexColor, + position: r.position, + memberCount: r.members.size, + managed: r.managed + })).sort((a, b) => b.position - a.position); + + res.json({ roles }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +/** + * Helper: Channel type to human name + */ +function getChannelTypeName(type) { + const types = { + 0: 'Text', + 2: 'Voice', + 4: 'Category', + 5: 'Announcement', + 10: 'Announcement Thread', + 11: 'Public Thread', + 12: 'Private Thread', + 13: 'Stage', + 14: 'Directory', + 15: 'Forum', + 16: 'Media' + }; + return types[type] || `Unknown (${type})`; +} + +module.exports = router; diff --git a/services/arbiter-3.0/src/routes/admin/index.js b/services/arbiter-3.0/src/routes/admin/index.js index d080428..8722619 100644 --- a/services/arbiter-3.0/src/routes/admin/index.js +++ b/services/arbiter-3.0/src/routes/admin/index.js @@ -12,6 +12,7 @@ const graceRouter = require('./grace'); const auditRouter = require('./audit'); const rolesRouter = require('./roles'); const schedulerRouter = require('./scheduler'); +const discordAuditRouter = require('./discord-audit'); router.use(requireTrinityAccess); @@ -77,5 +78,6 @@ router.use('/grace', graceRouter); router.use('/audit', auditRouter); router.use('/roles', rolesRouter); router.use('/scheduler', schedulerRouter); +router.use('/discord', discordAuditRouter); module.exports = router;