TDDiscordBot

A Discord bot that forwards Elite Dangerous Background Simulation (BGS) tick notifications from Zoy’s Tick Detector to Discord servers. Each server independently configures which in-game faction it wants to track.


How it works

The bot subscribes to Zoy’s Tick Detector via ZeroMQ and receives real-time BGS events (~10,000/day). When an event affects a system belonging to a server’s configured faction, the bot posts a notification to a dedicated read-only channel in that server.

Events forwarded:

Event Description
GalaxyTick First tick detection of the day — marks the start of a new BGS cycle
SystemTick A system has ticked (influence/state change detected)
FactionChanges A faction has arrived in or retreated from a system
FactionExpandedFrom A faction has expanded outward from a system

Prerequisites

  • Node.js 18+ (via nvm recommended)
  • PostgreSQL 16+ with the ed_tickbot schema (see Database Setup)
  • A Discord application with a bot token (see Discord Setup)
  • Network access to Zoy’s Tick Detector on the same VPS (ZMQ local TCP)

Discord Setup

1. Create a Discord Application

  1. Go to the Discord Developer Portal
  2. Click New Application — give it a name (e.g. ZoyTDBot)
  3. Go to the General Information tab and copy the Application ID — this is your DISCORD_CLIENT_ID

Before you can mark the bot as private, the portal requires the install link to be cleared first — if you skip this step you’ll get a “Private application cannot have a default authorisation link” error.

  1. Go to the Installation tab
  2. Under Install Link, change the dropdown from Discord Provided Link to None
  3. Save

3. Create a Bot and get the token

  1. In your application, go to the Bot tab
  2. Click Reset Token (or Add Bot if first time) — copy the token immediately, it is only shown once
    • This is your DISCORD_TOKEN — treat it like a password, never commit it
  3. Uncheck Public Bot — this bot is invite-only; the setting only saves cleanly after step 2 above
  4. Leave Requires OAuth2 Code Grant unchecked
  5. Leave all Privileged Gateway Intents unchecked — the bot doesn’t need Presence, Server Members, or Message Content

4. Set Bot Permissions

The bot requires these permissions in any server it joins:

  • Manage Channels — to create the #zoy-td-tick-bot channel on join
  • View Channel
  • Send Messages
  • Manage Messages — to bulk-delete messages older than 3 days

5. Generate an Invite URL

  1. Go to the OAuth2 → URL Generator tab
  2. Under Scopes, select bot and applications.commands
  3. Under Bot Permissions, select the four permissions listed above
  4. Copy the generated URL — use this to invite the bot to servers

Database Setup

Run the following scripts against your PostgreSQL instance as a superuser:

psql -U postgres -d your_database -f schema/01_role.sql
psql -U postgres -d your_database -f schema/02_schema.sql

Edit schema/01_role.sql first to set a strong password for the edtickbot_role role, and replace your_database with your actual database name.


First-Time Configuration

1. Copy the config template

cp src/config/config.mjs.template src/config/config.mjs

config.mjs is in .gitignore — it must never be committed.

2. Edit src/config/config.mjs

Fill in the following values:

config.discord.token    = 'your bot token from the Discord Developer Portal';
config.discord.clientId = 'your application ID from the Discord Developer Portal';

config.zmq.address = 'tcp://127.0.0.1:5555';  // port where Tick Detector publishes

config.postgres.user     = 'edtickbot_role';
config.postgres.host     = 'localhost';
config.postgres.database = 'your_database';    // same database used in schema setup
config.postgres.password = 'your role password set in 01_role.sql';
config.postgres.port     = 5432;

config.factionApi.baseUrl = 'http://tickapi.infomancer.uk';  // leave as-is unless self-hosting

3. Install dependencies

npm install

4. Register slash commands with Discord

This step sends the /setfaction, /status, and /help command definitions to Discord’s API. It only needs to be run once (or again if commands change).

npm run register

Global command registration can take up to an hour to propagate across Discord. For immediate testing, guild-scoped registration is faster — see Registering commands to a specific server.


Running the Bot

Development / direct Node

npm start

Production via PM2

pm2 start ecosystem.config.cjs
pm2 save
pm2 startup   # follow the printed instructions to enable auto-start on reboot

Logs are written to log/ with daily rotation (14-day retention).


Per-Server Configuration

Once the bot has joined a server, an administrator runs:

/setfaction <exact faction name>

For example:

/setfaction Sirius Corp

The faction name must match exactly as it appears in Elite Dangerous (case-insensitive lookup is used internally). The bot will confirm the faction and the number of systems it currently occupies.

Other commands:

/refreshfaction   — (Admin) immediately refresh the faction's system list, e.g. after a known expansion
/status           — shows last Tick Detector heartbeat and configured faction
/help             — lists available commands

What happens when the bot joins a server

  1. A channel named #zoy-td-tick-bot is created automatically
  2. The channel is locked — only the bot can post; members can read but not write
  3. A config row is created in the database for the server
  4. An administrator then runs /setfaction to configure which faction to track

The bot begins forwarding notifications immediately after /setfaction is run.


Message Retention

Messages in the bot’s channel are automatically deleted after 3 days. This runs daily at 03:00 server time. The 3-day window is a fixed architectural decision — it ensures Discord’s fast bulkDelete path is always used and keeps the channel clean.


Registering commands to a specific server

For faster testing during development, you can register commands to a single guild rather than globally. Edit scripts/register-commands.mjs and replace:

Routes.applicationCommands(config.discord.clientId)

with:

Routes.applicationGuildCommands(config.discord.clientId, 'YOUR_GUILD_ID')

Guild-scoped commands appear instantly. Remember to revert to global registration before wider deployment.


Tests

Tests use Vitest and are co-located with source files (*.test.mjs). No database, Discord, or network connection is required — all external I/O is mocked.

Run all tests

npm test

Run all tests with verbose output (per-test results)

npx vitest run --reporter=verbose

Run a single test file

npx vitest run src/processors/systemTick.test.mjs

Run tests matching a name pattern

npx vitest run -t "dispatches with systemAddress"

Test coverage by module

Test file Module under test
src/processors/heartbeat.test.mjs HeartBeat message handling, botState update
src/processors/systemTick.test.mjs SystemTick formatting and dispatch
src/processors/galaxyTick.test.mjs GalaxyTick: faction refresh ordering, dispatch
src/processors/factionChanges.test.mjs FactionChanges formatting and dispatch
src/processors/factionExpandedFrom.test.mjs FactionExpandedFrom formatting and dispatch
src/cache/guildCache.test.mjs In-memory config/system cache, DB load, reverse index
src/dispatcher.test.mjs System address → faction → guild routing
src/discord/publisher.test.mjs Rate-limited send queue, error handling
src/api/factionApi.test.mjs Faction API HTTP client, URL encoding, error cases

Project Structure

src/
  config/
    config.mjs.template     — copy to config.mjs and populate
  api/factionApi.mjs         — HTTP client for faction-systems lookup
  cache/guildCache.mjs       — in-memory guild config + faction→systems maps
  dao/db.mjs                 — PostgreSQL pool
  discord/
    client.mjs               — Discord client setup
    publisher.mjs            — rate-limited message send queue
    commands/                — slash command definitions and handlers
    events/                  — guildCreate, guildDelete, interactionCreate
  processors/                — per-topic message formatters
  zmq/subscriber.mjs         — ZeroMQ subscriber loop
  cleanup.mjs                — daily message deletion
  dispatcher.mjs             — routes system address to matching guilds
  main.mjs                   — entry point
scripts/
  register-commands.mjs      — one-shot Discord command registration
schema/
  01_role.sql                — PostgreSQL role creation
  02_schema.sql              — schema and table definitions
ecosystem.config.cjs         — PM2 configuration

License

MIT — see LICENSE