- JavaScript 84.1%
- PLpgSQL 10.4%
- Java 5.4%
- Shell 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .github/workflows | ||
| assets | ||
| commands | ||
| data | ||
| events | ||
| lib | ||
| minecraft-plugin | ||
| scripts | ||
| sql | ||
| .env.example | ||
| .gitignore | ||
| ANNOUNCEMENTS.md | ||
| check_mined.js | ||
| deploy-commands.js | ||
| DEPLOY.md | ||
| FEATURES.md | ||
| index.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| ROADMAP.md | ||
| TESTING.md | ||
After Hours Bot
A Discord bot built with discord.js v14 and Supabase (Postgres). Slash commands for a server economy (balance, jobs, daily/weekly claims), gambling games, moderation, and fun/text commands.
98 commands, registered globally. Website: https://afterhours.arshnah.in
Beyond the core bot, this repo also contains:
lib/api.js |
Read-only JSON API (/stats, /leaderboard, /mc-stats, /activity, /wealth) the website reads. Binds to 127.0.0.1:5090, exposed via nginx at jugaadapi.arshnah.in. |
lib/selfUpdate.js |
The jgdall deploy, runnable from Discord via /vps bot update. One of only two places the bot spawns a subprocess. |
lib/minecraft.js + lib/rcon.js |
Two-way Minecraft bridge. RCON written in raw net, no dependency. |
minecraft-plugin/ |
JugaadBridge, a Paper plugin that posts real server events to the bot. Built with plain javac — no Gradle or Maven. |
lib/familyTree.js |
/family tree rendered as a PNG via WASM Graphviz. |
scripts/test-*.mjs |
Standalone test scripts, no framework. node scripts/test-rcon.mjs etc. |
For anything Minecraft-related read JUGAAD-MINECRAFT.md in the parent
directory — architecture, every gotcha, roadmap, and outstanding work.
Requirements
- Node.js >= 22
- A Supabase project (Postgres database)
- A Discord application + bot token
- Privileged intent (required): welcome/goodbye use
guildMemberAdd/Remove, which require the Server Members Intent. Toggle it ON under your application's Bot tab in the Discord Developer Portal. Without it the bot fails to log in (disallowed intents). - Privileged intent (optional, automod only): the
automodword/invite filter needs the Message Content Intent. It's only requested when you setENABLE_MESSAGE_CONTENT=truein.envand enable the toggle in the portal. Leave it off and everything except automod content-scanning works normally. - The remaining intents (Guilds, Guild Messages, Message Reactions, Guild Voice States — the last for Focus Room) are not privileged.
- Image tools use
@napi-rs/canvas(installed vianpm install, ships prebuilt binaries — no native build step). The host needs at least one system sans-serif font for text rendering (standard on desktop and most Linux servers; on a minimal container, install e.g.fonts-dejavu).
Setup
- Install dependencies:
npm install - Copy
.env.exampleto.envand fill in the values:DISCORD_TOKEN= CLIENT_ID= GUILD_ID= SUPABASE_URL= SUPABASE_SECRET_KEY=DATABASE_URL(Supabase session pooler connection string) is also needed for running migrations — see Database. - Run the migrations (see below) so the schema exists before starting the bot.
- Register slash commands with Discord:
npm run deploy - Start the bot:
npm start
Database
Migrations are plain .sql files in sql/, applied in order with scripts/migrate.mjs. Each file is written to be idempotent (create ... if not exists / create or replace), so re-applying is safe.
node scripts/migrate.mjs --check # test the DB connection
node scripts/migrate.mjs sql/all.sql # apply the whole schema at once
node scripts/migrate.mjs sql/001_init.sql # or apply a single migration
sql/all.sql is every migration (001 → 023) concatenated in order — the one-shot way to set up a fresh database (or bring an existing one up to date, since it's idempotent). The individual sql/0NN_*.sql files remain the canonical numbered history; regenerate all.sql from them if you add a migration.
Commands
Commands live under commands/<category>/ and are auto-loaded by lib/loaders.js. npm run deploy registers one bucket, globally — npm run deploy:guild registers to GUILD_ID instead, and each mode clears the other so nothing shows twice. Installation context is set per command: the STATELESS_COMMANDS list in deploy-commands.js (fm, define, gif, action, …) is user-installable, so those work as a personal app in DMs and anywhere you are, while moderation, reaction and economy commands stay guild-only. Discord caps an application at 100 top-level commands; subcommands don't count.
A user-installable command shows up in servers the bot was never added to, where Discord only allows an ephemeral answer — lib/installContext.js handles that, and scripts/test-install-context.mjs covers it. Note the list is STATELESS_COMMANDS; this paragraph called it PERSONAL_APP until 28 Jul 2026, and that name no longer exists in the code.
Prefix commands: 75 of the 98 run from a . or ah text prefix (e.g. .daily, ah mc status, .ban @x spam). Subcommands work too — the first word after the command picks one. The allowlist and the reasons for every exclusion live in lib/prefixBridge.js; scripts/test-prefix.mjs re-checks every entry against the real commands so a bad addition fails there rather than in chat. Needs the MessageContent intent (ENABLE_MESSAGE_CONTENT=true + portal toggle); the slash versions don't.
Owner-only: /vps carries everything that reaches the VPS — the bot process, the deploy, and the Minecraft server's controls. It is hidden from everyone (setDefaultMemberPermissions(0)), works in DMs, and is gated on OWNER_IDS rather than any Discord permission. See Owner controls.
Setup
/setup — a guided flow through every guild_settings config option (log channel, mod log, welcome, goodbye, confessions, pulse, starboard, autorole, automod, AFK renaming, economy scale) from one command instead of nine separate ones. ManageGuild, ephemeral. Uses Discord's native channel/role select menus, so picking a value never needs typing an ID. The standalone commands (/welcome, /starboard, …) still work — /setup calls the exact same getters/setters, it's just one place to find them. log_channel_id (the channel announceToLogChannel posts big wins/losses, birthdays, meme battle results, lottery draws and anniversaries to) had no dedicated command before this — /setup is the only way to configure it that isn't a raw DB write.
Economy scale is the one entry that opens a modal instead of a picker — a payout multiplier and a cooldown multiplier (each 0.1–10, default 1) stored in guild_settings.economy_config, read by /daily, /weekly, /work and the level-up reward (lib/economyConfig.js, 60s cache like auto_messages). A ten-person server and a thousand-person one want different numbers; this scales the existing constants in data/economyConfig.js / data/levelingConfig.js rather than replacing them, so every guild keeps today's payouts until an admin changes it.
Economy
balance, daily, weekly, job, work, pay, leaderboard (type: richest | level)
Investing
invest market, invest buy <ticker> <amount>, invest sell <ticker> <shares>, invest portfolio [user] — fake tickers (CHAI, CRYP, MEME, GOLD) with daily-updated prices, cost basis, and unrealized P/L.
Leveling
Passive XP on messages (rate-limited per user); reaching a new level pays level × 50 Rokda and posts a level-up embed. rank shows your (or another member's) level, XP, rank, and progress bar.
Gambling
blackjack, coinflip, crash, dice, elimination, lottery, race, roulette, slots
Moderation
ban, kick, lock, lockdown, modlog, nickname set|reset, purge, role add|remove|info, slowmode, timeout, unban, unlock, unlockdown, warn add|list|remove|clear, automod toggle, autorole set|disable
/warnings was folded into /warn list. From a text prefix the old words still work — ah warns and ah unwarn are aliases (lib/prefixBridge.js), because a slash command has a menu to browse and a text prefix has nothing.
Two command budgets, not one. Discord allows 100 chat-input commands and a separate 5 message context menus. Chat input is at 98, so count before adding one. Translate is a message context menu (right-click → Apps) and costs nothing from the 98 — which is a reason to reach for one when the command acts on a message anyway.
Social
marry @user, divorce, marriage [user] — propose with a consent button; married partners get a +10% /daily bonus and an anniversary shoutout.
Fun
8ball, activechat, compliment, joke, pickup, rate, roast, ship, td, wyr, gif <search>, define <word>
gif searches Giphy (SFW-filtered) with a 🔀 shuffle button. Needs GIPHY_API_KEY in .env — without it, /gif just tells users it isn't set up.
Music (Last.fm + ListenBrainz)
A .fmbot-style suite under one /fm command (slash) plus . text aliases. A user can link a Last.fm account (up to two, merged), a ListenBrainz account, or both at once — every subcommand below merges across whatever's linked.
/fm … |
. alias |
Does |
|---|---|---|
np |
.fm |
now playing (merges every linked account) |
recent |
.rc |
last ~8 scrobbles/listens |
artist [name] |
.a |
artist info + your plays + tags |
album [name] |
.ab |
album info + your plays |
track [name] |
.tr |
track info + your plays |
top <type> [period] |
.ta / .tab / .tt |
top artists / albums / tracks |
whoknows [artist] |
.wk |
who in the server listens |
profile [user] |
.stats |
scrobble profile overview |
set [username] [second] [listenbrainz] |
.login / .lbset |
link account(s) |
Every linked account merges the same way now-playing always did (a currently-playing track wins, priority to the first account; otherwise whichever scrobbled/listened most recently). lib/fmSources.js is the dispatch layer — it turns a Discord user id into a flat account list and routes each per-account call to lib/lastfm.js or lib/listenbrainz.js.
Last.fm needs LASTFM_API_KEY in .env (last.fm/api) — without it, Last.fm-linked accounts and the catalogue-only artist/album/track subcommands (bio, tags, global listener counts — ListenBrainz has no equivalent) don't work, but np/recent/top/whoknows/profile still work fine for ListenBrainz-linked accounts. ListenBrainz needs no key or setup at all — its read API is public (api.listenbrainz.org); /fm set listenbrainz:<username> or .lbset <username> is enough. ListenBrainz's per-artist/album/track playcounts come from its stats endpoints (top-1000 all-time, since there's no single-item lookup), so a very lightly-played track can come back as 0 even when it truly has plays.
The . text commands additionally need the MessageContent intent (ENABLE_MESSAGE_CONTENT=true + portal toggle); the slash /fm commands work without it.
Community & Engagement
poll, suggest, reactionrole add, welcome set, goodbye set, confess, confess-settings set, meme submit|leaderboard, giveaway start, trivia start, birthday set, focus start|setroom|leaderboard, studybuddy join|leave|match, pulse setchannel, starboard set, remind, reminders, afk
- Reaction roles bind an emoji reaction on any message to a role (react to get it, un-react to lose it).
- welcome/goodbye post an embed when members join/leave; autorole hands new members a role on join (all need the Server Members Intent — see Requirements).
- confess posts anonymously to a configured channel (rate-limited 1/10 min; the author's id is never stored).
- meme runs a weekly 🔥-voted battle; the prize pool is auto-funded by a 2% skim of gambling losses.
- giveaway runs a timed 🎉-reaction draw; trivia is a 20s A/B/C/D question with a speed bonus.
- birthday greets + gifts Rokda daily; focus pays 2 Rokda/min for Pomodoro voice sessions.
- studybuddy matches opted-in members by shared tags and DMs both; pulse posts a weekly recap.
- starboard reposts messages that hit ⭐×N; remind sets DB-backed reminders (survive restarts); afk flags you away and auto-clears on your next message.
Utility
avatar, userinfo, serverinfo
Minecraft
mc status | whitelist | me | skin | stats | seed — all open to everyone. status shows who is online with per-player session time, server uptime and the MOTD; stats reads the server's own stats files for deaths, blocks mined, distance walked and the rest, for you or anyone named; seed is published deliberately rather than gatekept. The owner-only operations live under /vps mc.
Owner controls
/vps — hidden at the Discord level, usable in DMs, gated on OWNER_IDS:
/vps bot panel |
DMs a button panel for the lot |
/vps bot status |
Uptime, memory, gateway ping |
/vps bot restart |
Exits; PM2 brings it back |
/vps bot update |
The whole jgdall: pull, npm ci, migrations, optionally command deploy + plugin build + Minecraft restart. in_minutes: defers it and DMs the result |
/vps bot scheduled |
Show or cancel a deferred deploy |
/vps mc say |
Broadcast in game, with a colour picker and stackable styles |
/vps mc weather / time |
Sent namespaced (minecraft:weather) so EssentialsX cannot intercept |
/vps mc restart |
Warned countdown via restart.sh |
/vps mc whitelist |
Whitelist any name, for an account that is not the caller's |
/vps mc unwhitelist |
Remove and release the claim |
Every one of them asks for confirmation first.
Images & Meme Tools
Anime reaction gifs via the nekos.best API: /action <type> [user] — one command, 25 reactions (hug, pat, kiss, cuddle, slap, poke, tickle, feed, high-five, bite, punch, kick, handhold, wave, wink, peck, yeet, stare, shoot, dance, cry, laugh, blush, pout, smug).
Avatar/canvas tools (via @napi-rs/canvas): wanted, deepfry, jail, wasted, pixelate, glitch, pfp-card, caption, achievement, ascii, ship-card.
See ROADMAP.md for what's next (self-serve plugin, QoL "vanilla line", parked ideas), FEATURES.md for the fixes/features changelog, DEPLOY.md for the VPS deploy runbook (clone → migrate → deploy → PM2, plus the GUILD_ID gotcha), TESTING.md for the post-deploy test checklist, and ANNOUNCEMENTS.md for the #bot-insights drafts — one goes at the top of that file whenever a batch lands.
Project structure
commands/ slash command definitions, grouped by category
events/ discord.js client event handlers
lib/ shared logic (game engines, wallet, moderation, Supabase client, command/event loaders)
data/ static content and config (jokes, roast lines, job list, gambling config, ...)
scripts/ one-off/maintenance scripts (migrations, load testing)
sql/ numbered database migrations
Scripts
| Command | Description |
|---|---|
npm start |
Run the bot |
npm run deploy |
Register slash commands with Discord |
npm run migrate |
Run scripts/migrate.mjs |