Discord BotsEvidence Bot

Evidence & Case Management Bot

Setup, configuration, commands, punishment imports, appeals, addons, and troubleshooting for the ZCraft Studios Evidence & Case Management Bot.

Evidence & Case Management Bot

A Discord bot for recording evidence, reviewing appeals, and tracking staff work in a dedicated Forum channel. Each case has a case ID, a forum thread, a status, and a locally stored record. LiteBans and AdvancedBan can create cases automatically from their MySQL punishment records.

Built with Node.js, discord.js v14, and dotenv.

Get it on BuiltByBit or browse all ZCraft Studios resources at bbyb.it/zcs. Need help? Join the ZCraft Studios Support Discord.

The bot records moderation decisions only. It never issues bans, mutes, kicks, or unbans, and it does not run Minecraft server commands. Moderation actions stay in your Minecraft punishment plugin. Selecting Banned, Muted, Kicked, or Accepted only records the outcome.

Features

  • Evidence reports: forum post with in-game name, Discord identity, reason, actions, evidence, status, and reporting staff member. Optionally attach a screenshot or file.
  • Case IDs: timestamp-based IDs such as ZC-MO44GYQC.
  • Status organization: colored embeds, status prefixes in thread names, and matching forum tags. Unrelated tags are preserved up to Discord's five-tag limit.
  • Follow-ups: append notes, extra evidence, attachments, and status changes. Old and new statuses are recorded.
  • Assignments and queues: assign cases to staff reviewers, and let each reviewer download their outstanding queue.
  • Review reminders: automatic reminders when assigned cases sit inactive.
  • Player history: pull every stored case for a Minecraft name, UUID, or Discord user ID.
  • Case lifecycle: close and reopen cases with a recorded reason; closing archives the thread.
  • Timelines and exports: download a case's recorded timeline as JSON or a full text transcript.
  • Case search: by case ID, player, UUID, Discord identity, appeal ticket, or filing staff, with an optional status filter.
  • Member lookup: members can view cases tied to their Discord identity and check their own appeal status.
  • Statistics: status totals, recent months, and filing staff activity.
  • LiteBans and AdvancedBan import: new punishments create cases automatically from MySQL/MariaDB, read-only.
  • Appeal intake: imports appeal embeds from a configured channel, restricted to trusted webhooks and staff, with duplicate detection by ticket ID.
  • Appeal linking and decisions: link appeals to original cases, then accept, deny, or hold them with an optional note.
  • Activity logging: optional summaries to a log channel.
  • Forum synchronization: scans active and archived posts on startup and every five minutes.
  • Data migration: an explicit, dry-run-capable tool to import older JSON or SQLite case data.
  • Optional addons: a Staff Dashboard website and a Player Appeals website, each sold and installed separately.
  • Readable terminal output: concise INFO/WARN/ERROR logs with plain-language explanations and a suggested next step.
  • Local persistence: case data lives in data/cases.json; no external case database.
  • Instance lock: blocks a second process against the same installation.
  • Editable translations: English and Spanish message catalogs included, with a .env language selector, your own custom catalog, and automatic English fallback for anything missing or broken.

Command responses are ephemeral (visible only to the person who ran them). Case posts and activity logs follow the visibility of their Discord channels.

Requirements

  • Node.js 22.13 or newer, and npm. SQLite migration uses Node's built-in SQLite reader, so no extra runtime dependency is needed.
  • A Discord application with a bot token
  • A Discord server with a Forum channel for case posts
  • Permission to install and configure the bot in that server
  • A host with a persistent, writable directory and internet access to Discord
  • Optional: a MySQL/MariaDB database used by LiteBans or AdvancedBan, with a SELECT-only user for the bot

The bot is configured for one server and one evidence forum per running installation, with one bot instance per installation. The base package has no website, dashboard, or HTTP listener; those come from the optional addons.

Discord setup

Create the application

  1. Open the Discord Developer Portal and create or select an application.
  2. In Bot settings, get the bot token. Save it privately for TOKEN.
  3. Enable Message Content Intent.

Install the bot

Use an install link with the bot and applications.commands scopes, and grant the permissions below. Give members permission to use application commands.

Create channels and staff roles

Create a Forum channel for evidence cases. Optional channels: appeal intake, activity log, and review reminders. Enable Discord Developer Mode to copy IDs: the server ID is GUILD_ID and the forum's channel ID is FORUM_CHANNEL_ID.

Create forum tags

Create the seven status tags listed below in the evidence forum.

Appeal posts can contain email addresses, punishment details, and submitted evidence. Keep the appeal forum private and set visibility for your intended audience only.

Required bot permissions

PermissionPurpose
View ChannelAccess the forum and configured text channels.
Send MessagesCreate forum posts and send logs or reminders.
Send Messages in ThreadsPost follow-ups and appeal decisions.
Embed LinksSend structured case embeds.
Read Message HistoryRead starter messages and recover forum records.
Create Public ThreadsAllow thread creation where applicable.
Manage ThreadsRename, tag, archive, and unarchive posts.
Attach FilesDeliver exports, timelines, and queue downloads.

Check channel overrides as well as the bot's server role. Thread visibility follows the forum's permissions.

Forum tags

Create tags named Banned, Muted, Kicked, Pending, Suspicious, Appeal, and Resolved. These names are sufficient.

Internal statusDisplay labelTag nameOptional env override
bannedBannedBannedTAG_ID_BANNED
mutedMutedMutedTAG_ID_MUTED
kickedKickedKickedTAG_ID_KICKED
pendingPending ReviewPendingTAG_ID_PENDING
suspiciousSuspiciousSuspiciousTAG_ID_SUSPICIOUS
appealAppealAppealTAG_ID_APPEAL
resolvedResolvedResolvedTAG_ID_RESOLVED

The TAG_ID_* overrides still work if you use custom tag names. Status changes preserve unrelated tags up to Discord's five-tag limit. A forum that requires tags will reject new cases if the status tag is missing.

Installation and configuration

Run all commands from the extracted bot directory (where package.json is).

Install dependencies

node --version
npm ci

Use Node.js 22.13 or newer.

Create the environment file

cp .env.example .env

Edit .env

At minimum, set the required values:

TOKEN=your_bot_token
GUILD_ID=your_server_id
FORUM_CHANNEL_ID=your_evidence_forum_id
STAFF_ROLE_IDS=your_staff_role_id,your_admin_role_id

.env.example lists every normal setting. Channel and role settings take IDs, not names or mentions. Boolean settings accept true or false.

Start the bot

npm start

Startup logs in, then registers the 17 core guild slash commands plus any installed addon commands automatically, before starting background services. CLIENT_ID is no longer needed: the application ID is detected from the logged-in bot.

Registration clears this application's global commands and its commands in every server it currently belongs to, then registers the current core and configured addon commands only in GUILD_ID — removing older commands and any global/guild duplicates. If you run independent bots for different servers, use a separate Discord application per installation. A registration failure stops startup; npm run deploy is still available for standalone registration and exits with a nonzero code on failure.

While starting, the console prints its current step (Discord connection, forum validation, command refresh, addons, background services), elapsed time, and a progress line every 15 seconds while a step is still running. A Discord command rate limit prints its estimated wait. Forum recovery and MySQL polling run in the background and don't hold up the bot becoming ready.

Environment variables

Restart the bot after changing any setting. Invalid required settings fail startup with a configuration error.

VariableRequired?Description
TOKENYesDiscord bot token. Keep private.
LOCALENoMessage catalog filename without its extension. en (default) or es are included; custom loads your own locale/custom.json. Restart after changing it or editing a catalog.
GUILD_IDYesDiscord server ID. Commands and intake are restricted to this server.
FORUM_CHANNEL_IDYesEvidence Forum channel ID in that server.
STAFF_ROLE_IDSRecommendedComma-separated staff role IDs. Administrators always have access. When blank, only administrators can use staff commands.
LOG_CHANNEL_IDNoCase activity log channel.
APPEAL_CHANNEL_IDNoChannel monitored for appeal embeds.
APPEAL_WEBHOOK_IDSNoComma-separated trusted webhook IDs accepted in the appeal channel. Other webhook and bot messages are ignored.
REVIEW_REMINDER_HOURSNoInactivity interval for review reminders. 0 disables them.
REVIEW_REMINDER_CHANNEL_IDNoReminder channel. Blank sends reminders in the case thread.
PUNISHMENT_POLL_SECONDSNoMySQL polling interval. Minimum 10, default 30.
PUNISHMENT_IMPORT_EXISTINGNotrue imports existing punishment records when a provider stream has no saved cursor. Default false starts with new punishments only.
ADDONS_DISABLEDNoList of installed addon IDs to disable.
DEBUG_ERRORSNotrue temporarily includes developer stack traces. Default false.
TAG_ID_*NoOptional overrides for the status tag IDs.

LiteBans and AdvancedBan connection settings are listed under Punishment imports.

If STAFF_ROLE_IDS is blank, staff commands are limited to administrators. Add your moderator role IDs so non-admin staff can use them.

Keep .env private and never publish a real bot token.

Stopping and running continuously

Press Ctrl+C to stop a foreground instance, and stop your process manager as well when migrating. Closing a foreground terminal ends the session unless your host manages the process. Use a host or service manager that supports long-running Node.js processes:

  • Working directory: the folder containing package.json and .env
  • Install command: npm ci
  • Start command: npm start
  • Persistent storage: keep the data/ directory and configuration across restarts and redeploys

There is no port to expose for the base bot. Run one instance per installation and never share one data store between multiple copies. A local instance lock prevents concurrent startup and protects migration writes, but it does not coordinate separate hosts.

First-run checklist

  1. Confirm the 17 core slash commands registered to the intended server.
  2. Run /evidence as a staff member with a short test report and an attachment.
  3. Confirm the forum post shows the case ID, embed, and expected status tag.
  4. Assign the case, add a note with /followup, then close and reopen it.
  5. Find the case with /searchcases and download it with /caseexport.
  6. If configured, submit a test appeal from an allowlisted webhook and confirm it is imported.
  7. If you enabled LiteBans or AdvancedBan, issue a test punishment and confirm a case appears once, including after a restart.
  8. Restart the bot and confirm data/cases.json still holds the case.

Commands

Access overview

CommandAccess
/evidence, /followup, /assigncase, /myqueue, /playerhistory, /linkappeal, /closecase, /reopencase, /casetimeline, /caseexport, /searchcases, /resolveappealStaff roles or Administrator.
/casestatsAdministrator by default in Discord; the runtime staff check also applies.
/setpresenceAdministrator only.
/ping, /mycases, /checkappealMembers. Results and output are limited to the caller.

All responses are ephemeral. Autocomplete is restricted to staff, except /checkappeal, which suggests only the member's own tickets.

Staff commands

CommandOptions and behavior
/evidenceRequired: player_username, discord_username, reason, actions, status. Optional: evidence, attachment. Creates a report and stores its original details.
/followupRequired: case_id. Supply at least one of note, new_evidence, update_status, or attachment. Stores notes, attachment metadata, and old/new statuses. Closed cases must be reopened first.
/assigncasecase_id, reviewer. The reviewer must be a staff member. The assignment is recorded and the reviewer is mentioned in the thread.
/myqueueDownloads your outstanding assigned cases with forum links.
/playerhistoryplayer: exact Minecraft name, UUID, or Discord user ID. Downloads all matching stored cases, including appeals and resolved cases.
/linkappealcase_id: the appeal. original_case: the original punishment or report case. Posts a link and records the relationship.
/closecasecase_id, reason. Marks the case resolved, records the reason, and archives the thread.
/reopencasecase_id, reason. Unarchives the thread, marks it pending review, and records the reason.
/casetimelinecase_id. Downloads the recorded timeline as JSON.
/caseexportcase_id. Downloads a text transcript of stored details, evidence references, follow-ups, links, assignments, and the timeline.
/searchcasesquery and/or status. Searches case IDs, names, UUIDs, Discord identities, ticket IDs, and filing staff. Up to 50 matches; the embed displays the first 10.
/resolveappealcase_id, decision (accepted, denied, pending), optional note. Records the decision. Pending appeals remain reviewable; accepted and denied appeals become resolved.
/casestatsStatus totals, recent months, and filing staff activity.
/pingMember command. Shows the Discord acknowledgement round trip, gateway heartbeat latency, and process uptime.
/setpresenceAdministrator only. status, optional activity_type, activity_name, activity_url. Changes the bot's presence for this process.
/evidence player_username:ExamplePlayer discord_username:@ExampleUser reason:Repeated rule violations actions:Issued a seven-day ban status:Banned evidence:https://example.com/evidence
/followup case_id:ZC-MO44GYQC note:Reviewed the recording and confirmed the report.
/assigncase case_id:ZC-MO44GYQC reviewer:@ModeratorName
/resolveappeal case_id:ZC-MO44GYQC decision:accepted note:Appeal reviewed and accepted.

Prefer a Discord user ID or mention when recording identities. Legacy names are matched exactly, not by substring.

Member commands

  • /ping shows the caller the Discord acknowledgement time, gateway heartbeat latency, and process uptime — useful as a quick "is the bot alive and responsive" check for anyone, not just staff.
  • /mycases shows up to ten cases associated with the member's Discord identity, including resolved records.
  • /checkappeal ticket_id:... checks the member's own appeal. Autocomplete suggests only their own tickets.

Follow-up notes may be visible to the matching appellant when they check an appeal. Write notes with that in mind.

Command latency and responsiveness

/ping measures a single lightweight reply, not the duration of every case operation — a fast /ping doesn't guarantee every command is equally fast, just that the bot's connection to Discord is healthy. Commands that take at least five seconds are logged in the terminal for diagnosis, so a slow /caseexport or /searchcases on a large case store is something you can actually see and investigate.

A few things keep normal commands responsive: optional activity-log posts never delay the /evidence or /followup confirmation itself; a thread status change combines any changed tags and the thread name into one edit instead of several; and autocomplete reads lightweight case summaries rather than pulling full evidence, follow-ups, and timelines for every suggestion.

Review reminders

Once per minute, the bot checks assigned cases with statuses Pending, Appeal, or Suspicious. After the configured inactivity interval (REVIEW_REMINDER_HOURS), it mentions the reviewer in REVIEW_REMINDER_CHANNEL_ID or in the case thread.

  • Successful reminders are saved and do not reset the case's last activity.
  • Repeated reminders wait another full interval.
  • Closed and resolved cases are excluded.
  • Failed notifications are retried on the next check.

Evidence files and exports

Use the attachment option on /evidence or /followup to attach a screenshot or other file. The bot posts its Discord URL and stores its name, URL, size, and content type. Exports contain file references, not downloaded file contents.

Discord attachment URLs may expire. Keep original evidence in persistent storage when permanent retention is required.

LiteBans and AdvancedBan MySQL imports

The bot can create cases automatically from LiteBans and AdvancedBan punishment records. Each provider has its own connection, so both can be enabled together. Configure your Minecraft plugin to use MySQL/MariaDB and give the bot a database user with SELECT access only. The bot only reads those tables and never modifies punishment data.

LITEBANS_ENABLED=true
LITEBANS_MYSQL_HOST=127.0.0.1
LITEBANS_MYSQL_PORT=3306
LITEBANS_MYSQL_USER=evidence_reader
LITEBANS_MYSQL_PASSWORD=your_password
LITEBANS_MYSQL_DATABASE=minecraft
LITEBANS_MYSQL_SSL=false
LITEBANS_TABLE_PREFIX=litebans_

Reads bans, mutes, kicks, and warnings under the configured prefix, with player names looked up from the history table.

Set the provider's MYSQL_SSL=true when the database supports TLS with a trusted certificate. Table names and prefixes are case-sensitive on some MySQL hosts.

How imports behave

  • Mapping: temporary and IP bans map to Banned, temporary mutes to Muted, kicks to Kicked, and warnings to Pending Review.
  • Case contents: the recorded player, UUID when available, reason, operator, punishment type, start date, and expiry. Discord identities are not available from these tables.
  • First connection: the bot sets a cursor per provider and table. With PUNISHMENT_IMPORT_EXISTING=false it skips rows already present; with true it starts at ID zero.
  • Polling: after that, it reads up to 100 new rows per stream every PUNISHMENT_POLL_SECONDS seconds.
  • Reliability: successful cases and cursors are saved locally, failed rows are retried before advancing past them, and stored import IDs prevent duplicates after a normal restart.
  • Scope: imports create cases for new punishment rows only. Later revocations, reason edits, and expirations are not synchronized into case statuses.

The adapters follow the published LiteBans database structure and AdvancedBan SQL definitions. Forks that use different columns need an adapter change; ADVANCEDBAN_HISTORY_TABLE only changes the table name.

Backfilling old punishments

Keep data/punishment-sync.json and data/cases.json together when moving the bot. Changing PUNISHMENT_IMPORT_EXISTING does not reset an existing cursor. To deliberately backfill old rows:

  1. Stop the bot and back up both files.
  2. Set PUNISHMENT_IMPORT_EXISTING=true.
  3. Remove only the desired provider's cursor entries from punishment-sync.json.
  4. Start the bot. Stored import IDs skip cases that were already imported.

Reusing or resetting punishment table IDs is outside the cursor model.

Website appeal intake

This is for appeals submitted by an external website that posts a webhook embed into your appeal channel. The bot only listens for a matching embed, so nothing arrives here unless you have a site posting one. The Player Appeals addon does not use this webhook route. It is its own website that submits through the bot's built-in appeal service, so you don't need APPEAL_CHANNEL_ID or APPEAL_WEBHOOK_IDS for it. See Player Appeals below.

Set APPEAL_CHANNEL_ID and allow the submitting website's exact webhook ID in APPEAL_WEBHOOK_IDS. Staff-authored embeds are also accepted; arbitrary member messages and other bot or webhook authors are ignored. Intake processes new messages one at a time and skips tickets already in the store. It does not scan history or react to edits.

The first embed must have the title ⛔ Ban Appeal and the author name ZCraft Network.

Field nameRequired?Notes
MinecraftYesPlayer's Minecraft username. Max 32 characters.
DiscordYesDiscord identity, preferably a mention or user ID.
Ticket IDYesMust be unique. Max 100 characters.
TypeYesPunishment or appeal type.
Punishment ReasonYesOriginal reason for the punishment.
Appeal ReasonYesAppellant's explanation.
EmailNoContact email shown in the appeal post.
Punishment DateNoDate of the punishment.
EvidenceNoSupporting links or notes.
Original CaseNoAn original bot case ID, optionally wrapped in backticks.

Field values must be at most 1,000 characters. A valid Original Case ID links the appeal automatically; otherwise staff can use /linkappeal. Case IDs and ticket IDs are distinct.

A matching message creates an [APPEAL] forum post, applies the Appeal tag if found, assigns a new ZC-... case ID, and stores the ticket link.

Optional addons

Addons are sold and purchased separately from the core bot. To install one, extract its ZIP directly into the core bot's addons/ folder, then configure it by following the instructions bundled inside that addon's own package — its own README and config.json, not the core .env. Addons are not part of the base release.

The base bot runs with an empty or missing addons directory, and each addon is independent of the others.

Each addon folder contains a config.json, an index.js, and its own supporting assets. On startup the bot discovers folders that contain a config.json. Folders beginning with _ or ., flat JavaScript files, and folders without a config are ignored.

  • Install, remove, or reconfigure: restart the bot. Live reload is not supported.
  • Disable: set "enabled": false in the addon's config.json, or list its ID in ADDONS_DISABLED.
  • Settings: each addon is configured in its own config.json, not in the core .env.
  • Failures are isolated: if an addon fails to start, the main bot and other addons keep running. Its commands reply with an unavailable message, and the problem is logged and explained.

Addons execute trusted code inside the bot process. Install only addons you obtained from ZCraft Studios.

Website listeners and domains

Both website addons report their actual listener address/port, public website URL, and OAuth redirect in the console on startup.

  • A missing, null, "auto", or 0 port picks an OS-assigned available port. An explicit port is honored, and startup fails if that port is already in use.
  • Local HTTP origins stay in sync with the actual listener port automatically. HTTPS origins (behind a reverse proxy) stay fixed.
  • A blank baseUrl/domain defaults to local access. settings.domain accepts a custom hostname (HTTPS by default); baseUrl is also still supported.

An automatically chosen port can change between restarts. Discord's registered OAuth redirect and any reverse-proxy configuration are external and are not rewritten automatically — update them yourself if the port changes. For production, use a fixed port and an HTTPS domain behind a reverse proxy, with that exact callback URL registered in Discord's OAuth2 redirects.

Light and dark website themes

Both addons include a color-mode toggle on the login page and in signed-in navigation. A visitor's explicit choice is saved in their browser and survives reloads.

  • Defaults: the Staff Dashboard defaults to dark; Player Appeals defaults to light.
  • Setting it explicitly: set settings.theme.mode to light, dark, or system in the addon's config.json. system follows the visitor's device preference until they pick a mode themselves. If mode is omitted, the configured background brightness decides the default.
  • Customizing each palette independently: optional settings.theme.light and settings.theme.dark objects let you set different colors for each mode, using the same six-digit hex color keys as the rest of the theme config:
{
  "theme": {
    "mode": "system",
    "light": { "primary": "#4769e8", "primaryText": "#ffffff" },
    "dark": { "primary": "#6b8afd", "primaryText": "#090e1b" }
  }
}

Place this under the addon's settings in its config.json. Colors are validated, and an existing flat color setting (not split into light/dark) applies to whichever palette matches its brightness.

Customizing appearance and text

Each addon exposes a validated whitelist of public theme and text settings — colors, fonts, and display text — rather than open-ended styling. A few things that follow from that:

  • Each addon's config.example.json lists exactly which theme and text keys are configurable. Settings outside that list are rejected with a readable configuration error instead of being silently ignored.
  • Customized text renders as plain text, not HTML, so theme or text customization can't be used to inject scripts into either website.
  • Listener addresses, ports, and OAuth secrets are never exposed through the public appearance settings — only visual/text customization is.
  • Leaving a theme or text setting unset falls back to that addon's default rather than failing.

Staff Dashboard

A staff-only website addon with Discord OAuth sign-in for statistics, evidence review, assignments, case management, exports, and appeal decisions. It installs into addons/appeals-dashboard/. The folder and addon ID keep that older name for compatibility. Setup steps are in the README inside that folder.

  • Player responses exclude private staff data.
  • Write requests are same-origin and CSRF-protected, and requests and submissions are rate limited.
  • Current Discord staff roles are checked on every staff action.
  • Case management actions and appeal decisions share the core appeal service with /resolveappeal, including duplicate-decision protection, stale-update checks, and closed-case guards.
  • Case data is cached and lookups are indexed for responsiveness; staff case histories load on demand and the list view loads in batches rather than all at once.
  • Sessions are memory-only and end when the bot restarts.
  • Production URLs must use HTTPS.

Player Appeals

A separate player-facing website addon. Players sign in with Discord, submit an appeal, and follow their own tickets and public review outcomes. It installs into addons/player-appeals/, doesn't depend on the Staff Dashboard, and adds no slash commands.

How it works:

  • The form collects the Minecraft name, punishment type and reason, an appeal statement, and optional evidence. Discord identity comes from the verified sign-in, not typed input.
  • Choosing an original case is optional. Without one, the bot matches the newest open punishment owned by that player for that name and type. Ownership always requires a recorded Discord identity.
  • If nothing matches, an unlinked appeal is created. Imported punishment records have no Discord identity, so staff verify the player and link them with /linkappeal.
  • Duplicate open appeals are rejected, and submission attempts are rate limited.
  • Intake and later decisions are posted into the original case thread and timeline without changing the original punishment status.
  • The site is player-only. Its API rejects staff queue and decision access, even for an authenticated administrator. Staff review through the core Discord commands or the Staff Dashboard.

It uses its own session cookies, OAuth callback, and listener, separate from the Staff Dashboard. See Website listeners and domains for ports and redirects.

Migrating older bot data

Migration is explicit and does not run during startup. Stop the old and new bot processes first. Keep an untouched copy of your original data, including SQLite -wal and -shm files if present, and stop the old process cleanly so pending writes are checkpointed.

Supported sources

  • JSON objects keyed by case ID, arrays of case records, or a { "cases": ... } wrapper.
  • Legacy SQLite databases with a cases table and an optional followUps table.

Migration preserves case IDs, thread IDs, original timestamps, available follow-ups, and extra fields. Common snake_case names and status aliases are normalized. Missing guild and forum IDs can be supplied with --guild and --forum, or read from .env. Unknown statuses, malformed records, or corrupt data abort before anything is written. Missing historical evidence and status changes cannot be reconstructed, and the timeline labels recovered legacy entries accordingly.

npm run migrate -- --source /path/to/old/data/cases.json --dry-run
npm run migrate -- --source /path/to/old/data/cases.json

Optional arguments: --target /path/to/destination.json, --guild SERVER_ID, --forum FORUM_ID. The default destination is this installation's data/cases.json.

  • Dry runs do not write any files.
  • Applying a migration takes the bot's instance lock, backs up an existing destination to a timestamped .bak file, and writes through an atomic rename.
  • When merging a separate source, existing destination IDs are skipped rather than overwritten, and totals are printed.
  • In-place migration upgrades existing records.
  • The command does not create Discord posts or change their tags.

After migrating, verify the printed counts, restart the bot, and run /searchcases.

Storage, recovery, and backups

Case records are stored in data/cases.json, and punishment cursors in data/punishment-sync.json. There is no external case database. Writes use a temporary file and rename, and a case file that cannot be parsed raises an error instead of silently replacing your evidence with an empty store.

Forum recovery

On startup and every five minutes, the bot scans active and archived public posts in the evidence forum. It can recover recognizable bot case IDs, player details, status, and starter evidence.

  • Existing records remain authoritative.
  • Recovery does not reconstruct follow-up messages, assignments, or complete audit history.
  • Deleted or inaccessible Discord threads do not delete stored records. Case exports remain available, while operations that need the thread report an error.

Backups and hosting moves

  1. Stop the bot.
  2. Back up the full data/ directory, .env, and any customized source or addon configs.
  3. Store backups securely; they can contain private information.
  4. On the new host, install dependencies and restore both case data and cursor data together.
  5. Start a single instance and verify case lookups and forum access.

data/bot.lock is a process lock, not case data. Do not restore it.

Updating

  1. Stop the instance and take a backup.
  2. Replace application files, keeping .env, data/, and addon configs.
  3. Run npm ci.
  4. Run npm start and check the console and a case command. Commands re-register automatically at startup.

Terminal output and diagnostics

The bot prints concise INFO, WARN, and ERROR lines. Common Discord, MySQL, network, and file errors are translated into plain language with a suggested next action, and startup problems such as missing dependencies, bad configuration, lock failures, and Discord login errors are shown without stack traces.

  • Identical background import errors are deduplicated for a minute so the console is not flooded every poll.
  • Known tokens, passwords, and secrets from your environment are redacted from log output.
  • Set DEBUG_ERRORS=true temporarily to include developer stack traces when you need them for support.

Verification and troubleshooting

npm test
npm audit

Automated tests use temporary data and mocked Discord and MySQL responses, so a real installation still needs the first-run checklist as a smoke test.

SymptomChecks and next steps
Bot cannot log inVerify TOKEN, internet access, and Node.js version. Get a new token if it was reset.
Disallowed-intent errorEnable Message Content Intent in the Developer Portal, then restart.
Commands missingCheck GUILD_ID, the bot and applications.commands install scopes, the startup registration result, and member command permissions.
Staff command deniedThe user needs a role in STAFF_ROLE_IDS or Administrator permission. Blank roles allow only administrators.
Invalid forum errorSet FORUM_CHANNEL_ID to a Forum channel ID in the intended server, not a text channel or thread.
Forum creation failsCheck the forum ID, channel permissions, and that all required status tags exist.
Follow-up or resolution cannot be postedCheck thread visibility, Send Messages in Threads, and whether the thread is locked or archived. Manage Threads may be needed.
Appeal ignoredCheck the configured channel, the allowlisted webhook ID or staff author, the exact embed title and author, and all required fields.
Old punishments not importedAn existing cursor, or the default PUNISHMENT_IMPORT_EXISTING=false. See Backfilling old punishments.
MySQL import failedCheck provider credentials, SELECT grants, TLS configuration, the exact table prefix and name, and the plugin schema. Failed rows are retried.
/mycases empty or appeal ownership failsCompare the stored Discord identity to the caller's ID. Use a real mention or ID when filing; imported records have no Discord identity.
Log or reminder messages missingVerify the channel IDs and the bot's permissions in those channels.
Migration refuses to runStop the running bot or process manager, then examine the lock PID.
Corrupt case storeStop the bot and restore a verified backup.
"Another bot instance is already running"Stop the existing process named in the message. Confirm it has stopped before removing a stale lock.
Addon commands say unavailableCheck the console for that addon's start error, then fix its config.json and restart.
An addon website's OAuth sign-in fails after a restartCheck whether its port auto-assigned a new value — the registered Discord redirect and any reverse-proxy rule need updating to match.

Editable bot translations

Select a language

The core package includes readable translation files under locale/:

FileLanguage.env setting
locale/en.jsonEnglishLOCALE=en
locale/es.jsonSpanishLOCALE=es
locale/custom.jsonYour own catalogLOCALE=custom

Set the desired language in the actual .env file, not just .env.example:

LOCALE=es

Restart the bot after changing the selection or editing a catalog. Startup prints the loaded language and filename. Existing Discord messages are not rewritten — only messages sent after the restart use the new language.

Customize messages

Copy an included file to locale/custom.json, then edit values under messages, keeping the English keys:

{
  "messages": {
    "Evidence recorded": "Evidencia registrada",
    "Reviewer assigned": "Revisor asignado"
  }
}
  • Partial catalogs are fine — any entry you don't translate just uses English.
  • Filenames accept letters, numbers, underscores, and hyphens, with an optional .json extension. A path (with / or ..) is not accepted.
  • Values must be non-empty strings of at most 2,000 characters.
  • Whatever formatting you put in a value is what displays — it's preserved as written.

Payload text is still bounded to Discord's normal limits. An overly long custom translation can get trimmed the same way any other field would.

English fallback

A missing file, malformed JSON, invalid filename, or invalid messages object falls back to the bot's built-in English — not the editable locale/en.json file, which means a broken or accidentally deleted English catalog can't take the fallback down with it. Missing, empty, non-string, and oversized individual values fall back the same way, one at a time, rather than failing the whole catalog. The terminal reports whenever fallback text is used, so a partially-broken custom.json is visible in the console rather than silently showing English for everything.

Translation coverage

Catalogs translate shared embed headings, field labels, catalogued reply phrases, status displays, and next-step text. What they don't translate:

  • Evidence text and notes inside code blocks
  • URLs and mentions
  • Ordinary field values (the actual reason, player name, etc. you typed in)
  • Stored status keys, forum tag names, webhook intake field labels, slash command names themselves, exports, addon websites, and terminal diagnostics — these all keep their original format regardless of LOCALE

Uncatalogued sentences remain in English. The included Spanish catalog doesn't yet cover every possible command sentence word-for-word.

Locale JSON files ship as plain, readable assets in the base package — they bypass JavaScript obfuscation and get the same checksum verification as other customer-editable files, so you can open and edit them directly.

Resources and support

When reporting a problem, include the command or action, Node.js version, relevant console error, and steps to reproduce. Remove bot tokens, passwords, email addresses, and private evidence from shared diagnostics.