The Discord companion for the TarkovTracker.org community: slash commands, reaction roles, welcome automation, ticket creation, and a public bug-intake portal that forwards reports straight to GitHub.
TrackerBot runs two services side by side:
| Service | Entry point | Responsibility |
|---|---|---|
| Discord bot | bot.js |
Slash commands, reaction roles, welcome automation, and ticket creation. |
| Bug intake web server | webserver.js |
Serves the public issue forms and forwards submissions to GitHub. |
Both report forms currently create issues in REPO_DEV, configured as
tarkovtracker-org/TarkovTracker. The data overlay repository is private, so
REPO_DATA_REPORT is no longer required and any existing value is ignored.
Data reports retain their category-prefixed title, category, reference and details.
Both forms link to the public TarkovTracker issue list for delivery checks and
explicitly warn that Discord handles, reference links and report details are public.
POST /data and POST /issue share one reporting policy for each client IP
(with the existing IPv6 /56 grouping). The allowance is 20 delivered reports
in a rolling 12 hours, with at least 10 minutes between deliveries. A
delivery exactly 12 hours old leaves the allowance; a new report is admitted
exactly at the 10-minute boundary. IPv4-mapped addresses use the same key as IPv4,
including dotted and hexadecimal IPv6 representations. Switching forms does not
reset either limit. Users sharing an IP or IPv6 prefix share these limits.
The server reserves a slot before awaiting GitHub, so concurrent requests cannot
both pass. Confirmed GitHub issue creation (201) consumes the allowance and
starts the cooldown at delivery completion. Validation, origin/parser errors,
honeypot submissions and proven delivery rejections do not count as delivered
reports. A separate shared five-attempt rolling minute budget runs before
body parsing and includes rejected/invalid requests. Denials return the longest
active attempt/window/cooldown wait rather than an earlier partial reset.
Timeouts, connection resets, unexpected provider responses and GitHub 5xx
responses are unconfirmed delivery, not proof that no issue was created.
They conservatively reserve allowance and cooldown. A hash of the same report is
held against automatic repetition for 12 hours, bounded by the same retention
window; this is not a durable idempotency guarantee. Check the published issue
list or contact support before repeating it. Different reports may be sent after
the ordinary cooldown if allowance remains. No automatic GitHub retry occurs.
Both forms preserve their entered fields on failure, disable double submission, and display safe typed server errors. Unknown network/proxy/success responses keep the draft and stop automatic repetition in that page. The displayed wait is advisory; every new request is checked by the server. Length limits are visible and enforced without silently truncating evidence: title 200, Discord handle 64, category 64, reference 500, details 8,000 characters; request bodies are capped at 32 KiB. Successful submission resets the form. Drafts remain in the open page, not persistent browser storage; copy them before reloading.
Errors use { error: { code, message }, retryAfterSeconds?, resetAt? }.
Admission errors use HTTP 429 (report_cooldown, report_window_limit,
attempt_limit, report_in_progress), capacity uses 503, and uncertain repeat
delivery uses 409. Wait errors include numeric Retry-After seconds and an ISO
resetAt. Validation/parser/origin errors use 400/413/415/403. Proven provider
rejections/connectivity failures use 502/503; unconfirmed delivery uses 504.
Provider throttling honors full Retry-After and exhausted-primary
x-ratelimit-reset waits, taking the longer when both apply. A shared process-local
provider hold prevents every client on both routes, including staged uploads, from
calling GitHub again before that reset. Local denials include any longer provider
wait, while known uncertain-report denials keep their check-before-repeating warning.
Secondary throttles without timing headers use GitHub's minimum one-minute guidance.
Neither server nor browser truncates a valid provider wait. Known client parser failures,
including parameter overflow, excessive nesting and unsupported formats, return
retryable typed 4xx errors; they are not uncertain GitHub delivery.
See GitHub rate limits
and body-parser errors.
Provider bodies, tokens, IPs
and report descriptions are never included in error output or delivery logs.
Rollout limitation: these controls are process-local memory, not durable or shared across workers/hosts. A restart clears allowances, cooldowns, uncertain-report holds and the shared provider throttle. There is no existing persistent limiter store or hosting manifest in this repository. State is capped at 10,000 clients and stale entries are removed; at capacity, expiry cleanup runs at most once per second. New clients receive a safe busy response when the remaining entries still hold active limits; active attempt budgets are not evicted to make room. Otherwise rotating keys could bypass the five-attempt allowance. Sustained traffic from 10,000 distinct active client keys can still exhaust this bounded table; operator-side abuse protection and the trusted-proxy boundary remain necessary. Strict enforcement across restarts or multiple processes needs an operator-approved persistent/shared store after hosting is established. This change adds no infrastructure or production settings and is not evidence that the deployed topology is ready for strict enforcement.
trust proxy = 1 remains unchanged. Before rollout, operators must verify that
the origin is reachable only through the intended trusted proxy and that it
overwrites client forwarding headers, and confirm the actual process topology.
Otherwise clients may spoof the limiter key or unrelated users may share one.
Do not fix that by trusting arbitrary forwarded headers. The deployed form origin
must also match the existing ALLOWED_ORIGINS setting. Credential, proxy-trust and
production configuration changes require separate approval.
Use a patched, supported Node.js 24 LTS release in production. Node 18/20 are end-of-life. The package engine floor is API compatibility, not security support. See maintenance and runtime policy.
- Ticket creation flow with private support channels
- Reaction-role assignment, welcome messages and auto-roles for new members
- Website-first sharing (
/tt url,/tt resources) and private command help - Previewed bot announcements/replies (
/message), without accidental mention pings - Confirmed bounded purges, private audit archives, and channel resets
- Honeypot anti-spam channel that auto-bans anyone who posts in it
- Public web forms that open GitHub issues for bug and data reports
npm ci
npm run buildThis project has no bundle or generated build output. npm run build syntax-checks
all JavaScript files without starting either service. CI runs the same command,
the tests, and npm run audit:prod on Linux and Windows; production dependency
advisories fail CI. Weekly scheduled CI and Dependabot updates keep new findings
visible even without code changes. Dependencies and external actions are exactly
pinned; ordinary updates have a seven-day cooldown, while security updates stay on.
Copy .env.example to .env and fill in every value:
- Discord bot basics:
DISCORD_TOKEN,ADMIN_ROLE_ID(comma-separated Discord role IDs granting/message,/channeland/nuke) - GitHub issue routing:
GITHUB_TOKEN,REPO_DEV=tarkovtracker-org/TarkovTrackerfor both forms. LegacyREPO_DATA_REPORTis ignored. - Channel IDs for automated posts:
WELCOME_CHANNEL_ID; optionalHONEYPOT_CHANNEL_ID(a hidden channel β anyone who posts in it is banned on sight, with a kick fallback; messages older than 16h are swept hourly) - Ticket destination: optional
TICKET_CATEGORY_IDselects the category for newly created private tickets, independently of the channel containing the ticket button. See Ticket placement. - Role IDs:
AUTO_ROLE_ID_1βAUTO_ROLE_ID_4(auto-roles for new members); optionalALLOWED_COMMAND_ROLE_IDSrestricts resource sharing only (unset/blank allows members who can post);PANEL_ADMIN_ROLE_IDis retired and ignored - Web server:
PORT,ALLOWED_ORIGINS(comma-separated origins, exact match)
The deployed
GITHUB_TOKENneeds issue-write access totarkovtracker-org/TarkovTracker. Prefer a fine-grained PAT with Issues: write on that repository only. Redirecting data reports removes the private-overlay dependency, but does not grant permissions to a token that cannot create website issues.
- Use
npm ci,npm run build, andnpm startin the hosting configuration.npm startlaunches one bot and one web process per invocation; its launcher is a runtime dependency, sonpm ci --omit=devis also supported. Verify the actual replica count and deployment overlap rather than inferring it from the command. - Set
REPO_DEV=tarkovtracker-org/TarkovTracker. Verify the deployed token's issue-write access without sharing credentials. Both report routes use that destination. - Correct the operator's origin-list typos (
https//is nothttps://). A valid list for the reported domains is:https://tarkovtracker.org,https://www.tarkovtracker.org,https://trackerbot.nivmizz7.dev,https://www.trackerbot.nivmizz7.dev. A preview hostname must be added separately if its forms are tested. Do not disable origin checking just to make a preview pass. - Verify that Node is not directly Internet-accessible, that the trusted proxy
overwrites forwarding headers, and that distinct visitors retain distinct limiter keys.
Leave
trust proxy = 1unchanged until the real proxy path is established. - Confirm a single serving web process, including during deployments, and explicitly accept that restarts clear limits, uncertain-delivery holds and provider throttles. If strict limits across replicas or restarts are required, approve a shared/persistent store before rollout.
- A successful
/healthresponse only proves the HTTP process is running, not GitHub delivery or correct proxy isolation. Coolify configuration and production credentials cannot be verified from repository tests.
Start everything at once:
npm startThis launches the bot and issue intake web server in parallel. Logs for each service are prefixed with bot or web.
Or run a single service:
npm run start:bot # Discord bot
npm run start:web # Public issue forms (http://localhost:3000)- Bot login success is logged as soon as Discord authenticates the token.
webserver.jsserveshttp://localhost:3000(issue portal), plushttp://localhost:3000/issueandhttp://localhost:3000/data(forms).
Set TICKET_CATEGORY_ID to a category ID in the same server, not the ID of the
text channel containing the Create Ticket button. Enable Discord Developer Mode,
right-click the desired category, and choose Copy Channel ID. Set the variable
in the bot host's environment and restart/redeploy the bot. Category names and
positions can then change without breaking ticket creation.
Existing installations need no new variable: unset or whitespace-only values use
the existing π --- Support --- category (case-insensitive, ignoring surrounding
whitespace). An explicit ID that is missing, belongs to another server, or points
to a non-category channel fails closed; it never silently falls back elsewhere.
Users are directed to staff instead of being told to create a category themselves.
The bot fetches the server's current channels before routing and checking for an
existing ticket in that category, rather than relying on an incomplete cache.
For a minimal server, keep one public ticket-entry channel under Support and
place private ticket channels in that same category; a separate Tickets category
is not required. By default, ticket permission overwrites remain explicit: the requester,
bot, and existing Discord Admin role receive access; @everyone is denied.
Server administrators retain Discord's inherent access. The bot still needs its
existing channel-management permissions; no new server permissions are granted to the bot.
For stable staff access, set optional comma-separated TICKET_STAFF_ROLE_IDS.
Each explicit role must exist in the same server and cannot be @everyone;
invalid explicit lists (including blank comma-separated items) fail closed.
Explicit ticket overwrites add only View Channel, Send Messages and Read Message
History, not management permission bits. Existing role/member permissions still
apply; these overwrites do not restrict existing powers. Choose dedicated basic
support roles if limited staff powers are required. Unset/blank preserves the existing name-based
Discord Admin fallback and its old management overwrite; no existing ticket
permissions are rewritten. Discord administrators retain inherent access.
Overlapping clicks from the same member cannot create two simultaneous tickets in this bot process. Ticket creation and notification-role toggles acknowledge privately before API calls. Failed opening/confirmation notices never claim that an already-created ticket was not created; check the actual channel before repeating an unconfirmed action. Locks are not shared across multiple workers.
Changing the destination only affects new tickets and duplicate checks there; it does not move, reopen, or delete existing tickets. Review outstanding tickets before changing destinations.
Keep durable setup guides, FAQs, credits/team information and known-issue details on the website, linked from a short Discord entry message. Use Discord for conversation, public help, feedback and private sensitive supportβnot duplicate reference channels. This is organizational guidance only; the bot does not reorganize the server or alter existing category permissions.
| Command | Description |
|---|---|
/tt url [private] [reply_to] |
Share the website with a clean embed and link button. |
/tt resources [private] [reply_to] |
Share maintained website guides and community tools. |
/tt help |
Private command guide, available to every member. |
/message content [title] [reply_to] [preview] |
Admin-only announcement/reply; defaults to a private Send/Cancel preview. Optional title sends a styled embed; otherwise plain text. |
/channel purge count |
Confirm deletion of up to 1β1000 recent messages, not an unbounded channel wipe. Discord skips messages over 14 days old. |
/channel archive |
Confirm staff-only text-channel archiving for audit, retaining history. |
/nuke |
Confirm a text-channel reset: clone settings/overwrites, then permanently delete the original and its history. IDs change; linked features may break. |
All commands explicitly use guild installation and guild context, not the
legacy/deprecated DM toggle. Administrative commands default to Discord
administrators only in the command picker. If your configured ADMIN_ROLE_ID
roles do not have Discord Administrator, grant them access in Server Settings β
Integrations β TrackerBot β Commands. Runtime checks still require a configured
admin role even when a Discord UI override grants visibility. Administrative
commands no longer require an unrelated hardcoded member role as well, and role
names never authorize these commands. @everyone is always rejected as an admin
role even if its ID is misconfigured. The old PANEL_ADMIN_ROLE_ID is unused.
Resource sharing is public by default. Public shares require the invoking
member to be able to post in that channel/thread. private:true remains available
in read-only channels and only shows the resource to the caller. Sharing can
optionally be restricted through ALLOWED_COMMAND_ROLE_IDS. Use specific roles;
@everyone is not a valid allowlist match. Leave it unset to allow any member
who can post.
reply_to accepts a message ID or a https://discord.com/channels/... link from
the same server and channel. A public message reply uses a private acknowledgement;
it never silently redirects to another channel or falls back to an unrelated post.
Private sharing cannot also target a public message. Replies and announcements
suppress all mention notifications, including the replied-to author.
/message previews show the actual plain-text or styled payload and expire after
one minute; Cancel/expiry sends nothing. Reply targets are validated before offering
a preview and refreshed before sending; missing targets never fall back to a post. Use
preview:false explicitly to skip the preview. Admin authority is refreshed before
sending and again after a preview confirmation. Delivery errors never claim success;
check the channel before repeating an unconfirmed send. For example:
/tt url
/tt resources
/tt resources reply_to:<paste a message link>
/message content:Please check the resources page. title:Helpful resources
/channel purge count:25
Optional HTTPS link overrides: TT_SITE_URL defaults to
https://tarkovtracker.org/; TT_RESOURCES_URL defaults to /resources on the
configured site's origin. Blank values keep defaults. Invalid URLs, non-HTTPS
schemes, embedded credentials and URLs over Discord's 512-character link-button
limit fail closed rather than publishing unsafe links.
No FAQ/status content is copied from the website into the bot.
Set optional ARCHIVE_CATEGORY_ID to a category in the same guild. Unset/blank
uses the existing Archive category (case-insensitive, trimmed); invalid
explicit IDs never fall back. /channel archive is deliberately not a delete:
it confirms the action, preflights the bot's View Channel, Send Messages, Read
Message History, Manage Channels and Manage Roles permissions, then atomically
moves the text channel while replacing
its overwrites with @everyone denied View Channel and only the configured admin
roles and bot allowed access. Discord administrators inherently retain access.
It never inherits a possibly public category's grants. Reviewers can inspect the
history before separately deciding to delete it. Threads are not individually
modified; existing thread memberships are not revoked.
Record original permissions before archiving if restoration may be needed. The confirmation warns that the bot does not save previous overwrites. Discord audit logs are only a limited aid, not a reliable permission backup or restoration method. One in-flight moderation action per channel is allowed in this bot process, including its confirmation. Multi-worker hosting requires a shared lock. Missing roles/categories, revoked authority and failed preflight refreshes do not trigger a fallback move. Treat an archive API error as an unconfirmed outcome: the channel edit may have applied even if its response was lost. Inspect the channel and its permission overwrites before retrying.
On the next deployed bot startup, the existing global bulk-registration replaces
the old catalog. /faq1 and /utd are removed: hardcoded stability/patch notices
age poorly and belong on the website. /put-member-role is removed: it was a
one-off mass mutation, not routine support; join auto-roles remain unchanged.
/wipe becomes bounded /channel purge count, and /archive becomes explicit
/channel archive. /nuke and /message remain recognizable. Stale invocations
receive a private pointer to /tt help and never run the removed actions.
This source change does not execute command registration, change production
settings, move channels or send live announcements during tests. /nuke refreshes
source settings after confirmation and refuses channels with flags that the
installed SDK cannot safely clone (for example, newer spoiler opt-in flags). Deploy/restart
when approved, review Integrations permissions, then perform non-destructive
smoke tests first. Official API references:
application commands,
interaction replies.
Existing button custom IDs remain recognized; availability depends on the role
safety checks below. Optional stable ID overrides survive role renames: REACTION_ROLE_SITE_ID, REACTION_ROLE_MONITOR_ID,
REACTION_ROLE_POLLS_ID, REACTION_ROLE_NEWS_ID, REACTION_ROLE_NOTIFS_ID.
Unset/blank values use the legacy names site, monitor, polls, news, notifs
(case-insensitive, trimmed). Invalid explicit IDs never silently fall back.
Use dedicated notification roles with no administrative/moderation powers.
Only ordinary view/send/history/reaction/embed/attachment/voice communication
capabilities and application-command use are permitted by the safety mask;
unknown future permission flags fail closed. Managed roles, @everyone, roles
above the bot, configured ADMIN_ROLE_ID roles, configured ticket staff roles,
and the legacy Discord Admin role cannot be self-assigned or removed through
these buttons. The bot must already have Manage Roles and the right hierarchy;
this code never grants itself server permissions.
Before upgrading, review legacy notification roles for unsupported flags such as
CreatePublicThreads and ChangeNickname. Members cannot assign or remove a
noncompliant role through its button. After reviewing the effects, remove
unsupported flags from a dedicated notification role or configure a separate
compliant role; never weaken the safety mask or remove necessary staff powers to
make a button work. Existing memberships are not automatically migrated or
removed. Staff must handle legacy opt-outs manually when retiring an old role.
Review each selected role's channel overwrites as well as its guild permissions before configuring it: a role can expose private channels even with zero base permission flags. Source-level checks cannot certify your live role/channel policy. Membership and role definitions are fetched before toggling, and rapid same-member toggles are serialized in this one bot process. A failed confirmation notice does not imply the role update failed. No existing panel is reposted and no live role/category permissions are edited by these tests.
trackerbot/
βββ bot.js # Discord bot entry point
βββ webserver.js # Public bug/data intake server
βββ src/
β βββ client/ # Discord client setup
β βββ commands/ # Slash command registration
β βββ config/ # Env validation and constants
β βββ handlers/ # Interaction and member event handlers
β βββ interactions/ # Slash and button handlers
β βββ utils/ # Shared helpers (e.g. card embeds)
βββ web/ # Static issue/data report forms
Issues and pull requests are welcome. Please open an issue to discuss substantial changes before submitting a PR.
Released under the MIT License.