• JavaScript 84.1%
  • PLpgSQL 10.4%
  • Java 5.4%
  • Shell 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Arshdeep 7e87292e57
Some checks are pending
Test / test (push) Waiting to run
ah math: let x work next to pi/e/tau too, not just digits
2026-08-05 21:05:07 +05:30
.github/workflows Wire up npm test + GitHub Actions CI, fix what it found 2026-08-05 16:58:11 +05:30
assets New avatar and a palette taken from it 2026-07-20 17:44:52 +05:30
commands Add ListenBrainz as a second /fm source, split the biggest help categories 2026-08-05 19:46:37 +05:30
data Fix bot invite permissions — the old link was missing moderation, roles, nicknames, message-manage 2026-07-25 15:33:29 +05:30
events Add ah math/calc/calculate — text-only, no slash command 2026-08-05 20:59:01 +05:30
lib ah math: let x work next to pi/e/tau too, not just digits 2026-08-05 21:05:07 +05:30
minecraft-plugin Stop tracking the plugin build output 2026-07-27 21:07:24 +05:30
scripts Add ListenBrainz as a second /fm source, split the biggest help categories 2026-08-05 19:46:37 +05:30
sql Add ListenBrainz as a second /fm source, split the biggest help categories 2026-08-05 19:46:37 +05:30
.env.example Add ListenBrainz as a second /fm source, split the biggest help categories 2026-08-05 19:46:37 +05:30
.gitignore Stop tracking the plugin build output 2026-07-27 21:07:24 +05:30
ANNOUNCEMENTS.md Bridge channel topic: per-guild, not home-guild only 2026-08-05 16:04:27 +05:30
check_mined.js feat(music): merge multiple linked Last.fm accounts in all stats & top commands; add check_mined.js script 2026-07-26 21:38:44 +05:30
deploy-commands.js Answer ephemerally in servers the bot was never added to 2026-07-28 14:52:51 +05:30
DEPLOY.md Server Activity Analytics: start collecting, bot side 2026-08-05 16:10:22 +05:30
FEATURES.md Add ListenBrainz as a second /fm source, split the biggest help categories 2026-08-05 19:46:37 +05:30
index.js Crash-proof event dispatch and add a process-level backstop 2026-08-05 15:29:49 +05:30
package-lock.json Configure command integration types and contexts to support personal app (User Install) scope and prevent duplicate commands 2026-07-26 12:11:35 +05:30
package.json Wire up npm test + GitHub Actions CI, fix what it found 2026-08-05 16:58:11 +05:30
README.md Add ListenBrainz as a second /fm source, split the biggest help categories 2026-08-05 19:46:37 +05:30
ROADMAP.md Add ListenBrainz as a second /fm source, split the biggest help categories 2026-08-05 19:46:37 +05:30
TESTING.md Wire up npm test + GitHub Actions CI, fix what it found 2026-08-05 16:58:11 +05:30

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 automod word/invite filter needs the Message Content Intent. It's only requested when you set ENABLE_MESSAGE_CONTENT=true in .env and 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 via npm 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

  1. Install dependencies:
    npm install
    
  2. Copy .env.example to .env and 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.
  3. Run the migrations (see below) so the schema exists before starting the bot.
  4. Register slash commands with Discord:
    npm run deploy
    
  5. 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 (001023) 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, globallynpm 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.110, 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