Background
Projects

Dark: Discord Bot & Dashboard

Screenshot of Dark: Discord Bot & Dashboard
28 September 2026
Dark is a Discord bot I built to run my own communities, and it grew into a full moderation, community and voice platform. It keeps the peace in a server: catching scams and leaked tokens before they spread, handling tickets and ban appeals, rewarding activity with levels and achievements, and running a live radio in voice channels. Alongside the bot is a web dashboard, so server staff can configure everything from a browser instead of typing commands. The dashboard runs inside the bot itself, so every change uses exactly the same data and code paths as the slash commands. There is no second copy of anything to keep in sync. Moderation and security. Bans with appeal DMs, timeouts, warnings with automatic escalation, case numbers, purge, lockdown and channel nuke. On top of that sit several overlapping protection layers:
  • Scam guard: spots the image bursts hacked accounts post with @everyone, times the account out and opens a case.
  • Leak guard: deletes pasted bot tokens, webhooks, cloud keys and private keys, then DMs the person instructions for revoking them.
  • Honeypot channel: anyone who posts in it is banned automatically; staff accounts trigger an alert instead.
  • Captcha verification, and a role-safety check that refuses to auto-assign roles carrying dangerous permissions.
Support. Ticket panels with claiming, auto-assignment, idle auto-close and HTML transcripts; modmail that turns DMs into tickets; and ban appeals. Community. XP for chat and voice with role rewards and a public leaderboard, achievements, reputation, starboard, counting, suggestions, polls, giveaways, reaction-role menus, welcome and boost banners, and social alerts for YouTube, Twitch, Kick, Reddit, Bluesky and GitHub releases. Voice. Join-to-create temporary channels with an owner control panel, and a radio that streams stations into voice channels and can talk over the music with pre-recorded voice lines. Every card Dark posts is drawn on the fly from SVG, in its crimson "watching" theme, then rendered to PNG in a worker thread so the bot never stalls while drawing.
Dark's rank card, showing a member's level, XP progress and server rank
Dark's profile card, with level, messages, voice time, reputation and XP
Dark's level-up card announcing a new level and an unlocked reward role
Dark's welcome banner for a new member
The dashboard has 43 settings pages, grouped into overview, settings, community, tools and moderation. Staff sign in with Discord, pick a server, and can change anything from welcome messages to ticket panels, schedule announcements, build embeds with a live preview, and review cases, warnings and ticket transcripts. Every save is recorded in an audit history and posted to the server's mod log, so changes are never silent.
  • Bot: Node.js with discord.js v14 and @discordjs/voice, about 80 slash commands (roughly 250 subcommands) and 9 right-click context-menu actions.
  • Dashboard: a plain Node http server with no web framework, rendering HTML on the server. Every page ships with a strict Content Security Policy, and most pages run no JavaScript at all.
  • Login: Discord OAuth2, with access re-checked on every request.
  • Images: SVG rendered to PNG with resvg, in worker threads.
  • Audio: ffmpeg plus a custom PCM mixer; voice lines are generated offline with Piper text-to-speech.
  • Storage: JSON files written atomically, with corrupt files quarantined rather than silently reset.
The dashboard only needs a Discord token long enough to read who you are and which servers you're in. So it uses the token once, then revokes it immediately, even if something fails along the way.
JavaScript
async function completeLogin({ code }) {
  const accessToken = await exchangeCodeForToken(code);
  try {
    const headers = { Authorization: `Bearer ${accessToken}` };
    const [me, guilds] = await Promise.all([
      discordFetch(`${API}/users/@me`, { headers }),
      discordFetch(`${API}/users/@me/guilds`, { headers }),
    ]);
    return normalise(me, guilds);
  } finally {
    // The token isn't needed any more: revoke it so it can't be misused.
    revokeToken(accessToken).catch(() => {});
  }
}
When Dark speaks in a voice channel, the music fades down, the voice line is mixed in sample by sample, and the music fades back up, like a radio presenter. The core of the mixer, simplified:
JavaScript
for (let i = 0; i < chunk.length; i += 4) {
  // ease the music volume towards its target (ducked while speaking)
  gain += Math.sign(target - gain) * Math.min(step, Math.abs(target - gain));

  let left = chunk.readInt16LE(i) * gain;
  let right = chunk.readInt16LE(i + 2) * gain;

  if (voice) {                       // mix the voice line on top
    left += voice.readInt16LE(pos) * voiceGain;
    right += voice.readInt16LE(pos + 2) * voiceGain;
    pos += 4;
  }

  out.writeInt16LE(clamp16(left), i);
  out.writeInt16LE(clamp16(right), i + 2);
}
Dropdowns in the dashboard only list channels the signed-in staff member can view in Discord. Instead of passing the user through every function, each request carries its own context:
JavaScript
const als = new AsyncLocalStorage();

const runAs = (member, fn) => als.run({ member }, fn);

function viewerCanSee(channel) {
  const member = als.getStore()?.member;
  if (!member) return true;
  return channel.permissionsFor(member)?.has(PermissionFlagsBits.ViewChannel) ?? false;
}
  • Discord's 100-command limit. Dark has more features than Discord allows top-level commands, so related commands are merged under parent commands (like /settings) while each keeps its own handler.
  • Not getting the bot IP-banned. Discord bans bots that make too many failed requests, so Dark counts failed requests against Discord's limit, honours rate-limit headers when retrying, and caps how often it posts per user and per server.
  • Safely fetching links people post. Features that fetch URLs check the address at connect time, block private and internal ranges (including tricky IPv6 forms) and refuse redirects, so the bot can't be used to reach internal services.
  • Memory. Unused caches are switched off and old messages swept, while deliberately never sweeping the bot's own member object, which permission checks depend on.
  • Securing a framework-free web app by hand: CSRF tokens, single-use OAuth state, __Host- cookies, per-IP rate limiting, body-size limits, a host allowlist and sandboxed transcripts.
Dark runs the CyberUK communities and The Debug Den, and also powers The Debug Shop: the shop's catalogue, status banner and live reviews are all managed from Dark's dashboard. It's the largest project I've built so far, and the one that taught me the most about security, real-time audio and running something people rely on every day.
This site uses cookies for preferences and analytics. No ads, no selling your data.