feat: Add Discord audit routes to Arbiter

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
This commit is contained in:
Claude
2026-04-08 15:15:20 +00:00
parent 7cf0eec2db
commit e99ef3b942
2 changed files with 185 additions and 0 deletions

View File

@@ -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;

View File

@@ -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;