const discordManager = require('../discord/discordManager') const newsAnnounce = require('../discord/newsAnnounce') const modLog = require('../discord/modLog') const createLogger = require('../utils/logger') const log = createLogger('internal') const REVERSIBLE = new Set(['ban', 'mute']) // discord.js REST error code for removing a ban that no longer exists. const UNKNOWN_BAN = 10026 // POST /internal/config — called by the main server right after an admin // saves the Discord Bot panel, and by the bot's own bootstrap on startup // (via a GET to the server for the current config, then this same start/stop // logic locally). Body: { token, guildId, enabled }. async function setConfig(req, res) { const { token, guildId, enabled } = req.body || {} try { if (enabled) { if (!token || !guildId) { return res.status(400).json({ message: 'token and guildId are required when enabled' }) } await discordManager.start({ token, guildId }) } else { await discordManager.stop() } return res.json(discordManager.getStatus()) } catch (err) { log.error('setConfig failed', { message: err.message }) // Still 200 with an error status — the caller (admin panel) should surface // discordManager's status/statusDetail rather than treat this as a 5xx. return res.json(discordManager.getStatus()) } } // GET /internal/status — live connection state, polled by the admin panel. function getStatusHandler(req, res) { return res.json(discordManager.getStatus()) } // POST /internal/announce — called by the main server right after a news // post is published. Body: { title, excerpt, url, imageUrl }. async function announce(req, res) { const connection = discordManager.getConnection() if (!connection) return res.status(503).json({ message: 'Bot is not connected' }) try { await newsAnnounce.postAnnounce(connection.client, connection.guildId, req.body || {}) return res.json({ posted: true }) } catch (err) { log.warn('announce failed', { message: err.message }) return res.status(400).json({ message: err.message }) } } // POST /internal/mod-reverse — called by the main server when a staffer APPROVES // a moderation appeal (Phase 6d). Body: { discord_user_id, action_type, appeal_id }. // Reverses the Discord action: 'ban' → lift the ban, 'mute' → clear the timeout. // Idempotent-friendly: an already-lifted ban ("Unknown Ban") or a member who has // left the guild is treated as success (the desired end state already holds). async function reverseModAction(req, res) { const { discord_user_id: discordUserId, action_type: actionType, appeal_id: appealId } = req.body || {} if (!REVERSIBLE.has(actionType)) { return res.status(400).json({ message: 'action_type must be ban or mute' }) } const connection = discordManager.getConnection() if (!connection) return res.status(503).json({ message: 'Bot is not connected' }) const reason = `Appeal #${appealId} approved` try { const guild = await connection.client.guilds.fetch(connection.guildId) if (actionType === 'ban') { try { await guild.bans.remove(discordUserId, reason) } catch (err) { // Unknown Ban → already unbanned; anything else is a real failure. if (err.code !== UNKNOWN_BAN) throw err } } else { // mute: clear the timeout. If the member has left, there's nothing to clear. const member = await guild.members.fetch(discordUserId).catch(() => null) if (member) await member.timeout(null, reason) } await modLog.postReversal({ client: connection.client, guildId: connection.guildId, actionType, discordUserId, appealId, }) return res.json({ reversed: true }) } catch (err) { log.error('mod-reverse failed', { message: err.message, actionType, discordUserId }) return res.status(500).json({ message: err.message }) } } module.exports = { setConfig, getStatus: getStatusHandler, announce, reverseModAction }