Discord bot written in Node.js (using discord.js) for HammerTime
$ sudo npm install -g pm2
$ npm install
$ cp .env.example .env
$ nano .env # Fill in the neccessary environment variables
$ npm build
$ pm2 start pm2.json
src/webhook.ts is the bot's only entry point (npm start/pm2.json's HammerTimeBot:Webhook) -
it receives interactions over Discord's HTTP Interactions Endpoint rather than a gateway connection,
so there's no ShardingManager/gateway mode to speak of. Setting an Interactions Endpoint URL on the
application in the Discord Developer Portal is what actually switches interaction delivery over -
once set, Discord stops sending INTERACTION_CREATE over the gateway entirely for that application,
which is why this needs to be a real HTTP server rather than something that only starts reacting once
you're sure: there's no dual-running fallback. Don't set that URL against production until you're
confident in the webhook path (test it against a dev subdomain / secondary application first).
One-time production server setup:
deploy/nginx/→ symlink the config into/etc/nginx/sites-available/and/etc/nginx/sites-enabled/, matching how this project's other nginx configs are laid out on that host.- Expand (or issue) a cert covering the webhook subdomain, e.g. via
certbot --expand. sudo nginx -t && sudo systemctl reload nginx- Set
DISCORD_PUBLIC_KEY(the app's Ed25519 public key, from the Developer Portal),WEBHOOK_PATH_SECRET(a long random URL-safe token - see its doc comment insrc/env.tsfor how to generate one), and optionallyWEBHOOK_PORT(defaults to3939) andWEBHOOK_HOST(defaults to127.0.0.1, so only the reverse proxy in front of it can reach the endpoint) in the server's.env. pm2 startOrReload pm2.json --only HammerTimeBot:Webhookto bring upHammerTimeBot:Webhook(or replace its workers if it is already running, see Clustering).- Once confident locally/in staging, set the Interactions Endpoint URL to
https://<webhook subdomain>/<WEBHOOK_PATH_SECRET>in the Developer Portal - this is the actual cutover step (see above). The endpoint only serves interactions at that secret path (anything else, including/, 404s) so scanner/health-check noise never reaches signature verification.
Both pm2 apps in pm2.json (the webhook and the API server) run as a cluster of two workers that share
their port, so more requests can be handled at once and one worker can be replaced while the other answers. Nothing is kept in
memory between interactions, which is why any worker can take any of them.
A deploy runs pm2 reload, which replaces the workers one at a time: a new worker sends ready once it is listening (wait_ready),
and only then does the old one get its SIGINT, which makes it stop taking connections, finish the requests in flight and exit
(kill_timeout is how long it gets). Nothing is dropped, which a rehearsal under load with the real pm2.json confirmed.
The first deploy after switching from a single process deletes and starts the app once, which has a short gap, and says so.
Change instances in pm2.json for another number of workers. Each one has its own copy of what the process keeps in memory:
for the API server that is a cache of validated keys, and the rate limits, which are counted
per worker unless REDIS_URL or REDIS_HOST is set in packages/api/.env, then they are shared.
The same commands are also available outside Discord, as a public REST API and as an MCP server, both served by
packages/api at https://api.hammertime.cyou:
- REST API: one
POSTendpoint per timestamp command (/at,/in,/snowflake, ...) with the command's options as the JSON body,GET/PATCH /settingsfor the bot settings, andGET /mefor the key in use. Requests follow the settings of the Discord account the key was created for, and are counted like bot commands. The interactive reference is at api.hammertime.cyou, the OpenAPI document at/generated/api-docs.json(and in this repository asdocs/openapi/api-docs.json). - MCP server: the Streamable HTTP endpoint
https://api.hammertime.cyou/mcp, with one tool per command plussettings. It uses the same keys, and only offers the tools a key's permissions allow, so any MCP consumer can use it. It is listed in the MCP Registry asio.github.WentTheFox/hammertime. - Keys: created, rotated and revoked on the API page of the website, after logging in with
Discord. A key acts as one linked Discord account and has permissions (
timestamps:create,settings:read,settings:write), which the page lists together with the endpoints.
The commands, their options and the OpenAPI document are generated from src/commands.json, so the API can not drift from the bot,
and the bot can use the API itself for the timestamp commands (COMMANDS_API_URL and COMMANDS_API_KEY in .env, falling back to
its own logic when the API does not answer). The users, keys and settings stay in the website's database, the API server only talks
to it over HTTP. How it is built, configured and released is in packages/api/README.md.
New language contributions are welcome! They are handled through Crowdin. If you don't see your language listed, that means it's likely not supported by the Discord client itself, meaning translations would be pointless. If a language is available in Discord's settings but is not listed on Crowdin, or you want to help with the translations of an existing language, please join our Discord server and ask for your language to be added to the project in the #translator-signup channel. You will be given the Translator role and granted access to a language-specific channel for further discussion. This is necessary so that when new translations are needed for any potential new bot features, I have an easy way to reach everyone at once.
English and Hungarian translations have been included, so no translators will be needed for these two languages.
- 🇧🇬 Bulgarian
- 🇨🇳 Chinese Simplified
- 🇹🇼 ChineseTW
- 🇭🇷 Croatian: Volvone
- 🇨🇿 Czech
- 🇳🇱 Dutch: Jesse
- 🇫🇮 Finnish: nicohele
- 🇫🇷 French
- 🇩🇪 German
- 🇬🇷 Greek: Belle Bernice
- 🇮🇳 Hindi: Balajiasli
- 🇮🇩 Indonesian: Jackie
- 🇮🇹 Italian: RVG|𝓵𝓸𝓻𝔂
- 🇯🇵 Japanese
- 🇰🇷 Korean
- 🇱🇹 Lithuanian: Trent
- 🇳🇴 Norwegian
- 🇵🇱 Polish
- 🇧🇷 Portuguese, Brazilian
- 🇷🇴 Romanian
- 🇷🇺 Russian
- 🇪🇸 Spanish
- 🇲🇽 Spanish, Latin America
- 🇹🇭 Thai
- 🇹🇷 Turkish
- 🇺🇦 Ukrainian
- 🇻🇳 Vietnamese