Engineering docs also live in the bot repository at
docs/ for contributors working directly in that repo.Startup And Configuration
The bot starts insrc/index.js. It creates an ExtendedClient, calls client.start(), and then starts the Express sidecar with server(). client.start() is async but is not awaited by src/index.js, so Discord login, command deployment, optional Prisma/MongoDB connection, optional Top.gg autoposting, and the HTTP listener can initialize concurrently.
Required local setup:
- Run
npm i. - Copy
.env.exampleto.env. - Copy
src/example.config.jstosrc/config.js. - Fill in Discord application IDs/tokens, MongoDB URIs, guild IDs, stat channel IDs, developer user IDs, and staff role IDs.
- Set
DATABASE_URLfor Prisma CLI commands. The runtime client reads the MongoDB URI fromsrc/config.js, so keepDATABASE_URLaligned with the selected runtime URI when generating or pushing schema metadata. - Run
npx prisma generateafter installing dependencies or changingprisma/schema.prisma. - Run
npm run devfor nodemon ornpm startfor node. - Run
npm testbefore opening a PR.
CLIENT_TOKENandCLIENT_ID: production Discord bot token and application ID.DEV_TOKENandDEV_CLIENT_ID: development Discord bot token and application ID.MONGODB_URIandDEV_MONGODB_URI: MongoDB connection strings.DATABASE_URL: MongoDB connection string used by Prisma CLI commands such asprisma generateandprisma db push.GUILD_ID: support or production guild ID.DEV_GUILD_ID: guild used by ready-time slash/context-menu registration.TOPGG_TOKEN: optional; enables Top.gg autoposting insrc/functions/index.js.
src/example.config.jsis the runtime schema forsrc/config.js.handler.mongodb.togglecontrols whetherExtendedClient.start()callsconnectPrisma()fromsrc/handlers/prisma.js.- Prisma runtime queries use
config.handler.mongodb.uri.DATABASE_URLis only read by Prisma CLI tools. config.variables.dbNamestill exists for configuration compatibility, but the verified Prisma startup path does not read it; include the database name in the MongoDB URI instead.- The Express sidecar in
src/server.jslistens on0.0.0.0:8080and returnsBot is online! Join our discord here: https://discord.gg/rmqAhQz2quat/. ExtendedClientupdatesconfig.variables.channels.botGuildsandconfig.variables.channels.botUsersevery 30 minutes. Those IDs must point to editable channels inconfig.handler.guildId.PRODUCTIONdeserves extra care: environment variables are strings. Token, client ID, and guild ID selection compare against"true", but MongoDB URI selection usesprocess.env.PRODUCTIONtruthiness. WithPRODUCTION=falseas a string,config.handler.mongodb.uristill selectsMONGODB_URI. Verify the generatedsrc/config.jsvalues before running a bot.
Prisma Persistence
Persistence now runs through Prisma v6 with the MongoDB provider:prisma/schema.prismadefines the generated client models and maps them to existing MongoDB collections with@@map, such asecoschemas,users,badges,afks,xps,welcomes,tickets,guildschemas,chatbots, andjtcsetups.src/handlers/prisma.jsexports a singletonprismaclient andconnectPrisma(). The client is constructed withdatasourceUrl: config.handler.mongodb.uri.- Files in
src/schemas/**are compatibility modules that export Prisma delegates, for examplesrc/schemas/EcoSchema.jsexportsprisma.ecoSchemaandsrc/schemas/GuildSchema.jsexportsprisma.guildSchema. - The legacy
src/handlers/mongoose.jsfile remains in the tree, butExtendedClientimportssrc/handlers/prisma.js. Do not add new imports of the Mongoose handler unless Mongoose is intentionally restored as a dependency.
- Query filters are nested under
where, and writes are nested underdata. - Returned records are plain objects. They do not have Mongoose methods such as
save(),deleteOne(), or document-scopeddeleteMany(). - Current models do not define unique compound indexes for guild/user pairs. Existing code usually reads with
findFirst({ where: ... }), then updates or deletes by the returnedid. - MongoDB is schema-less, so this repo does not use
prisma migrate. Runnpx prisma generateafter schema edits, and usenpx prisma db pushonly when you intentionally need Prisma to sync MongoDB indexes or schema metadata.
Command And Component Architecture
Command loading is handled bysrc/handlers/commands.js:
src/commands/slash/**: loaded intoclient.collection.interactioncommandsandclient.applicationcommandsArray.src/commands/devOnly/**: loaded intoclient.collection.developercommandsandclient.developerCommandsArray.src/commands/prefix/**: loaded intoclient.collection.prefixcommands; aliases are stored inclient.collection.aliases.
src/handlers/components.js:
src/components/buttons/**: modules needcustomIdandrun.src/components/selects/**: modules needcustomIdandrun.src/components/modals/**: modules needcustomIdandrun.
src/contextmenus/** are read by src/utils/getLocalContextMenus.js, validated by src/events/validations/contextMenuCommandValidator.js, and registered at ready time by src/events/ready/registerContextMenus.js.
Slash command modules usually export:
src/commands/devOnly/** use the same command shape and set options.developers: true.
src/events/validations/**:
- Regular slash commands use
chatInputCommandValidator.jsandsrc/utils/getLocalCommands.js. - Developer-only slash commands use
devCommandValidator.jsandsrc/utils/getLocalDevCommands.js. devOnly: trueoroptions.developers: true: user must be listed inconfig.moderation.developers. Regular slash/context/component validators compare againstinteraction.member.id; the developer-command validator compares againstinteraction.user.id.- Missing or empty
config.moderation.developers: developer-only commands indevCommandValidator.jsare denied as misconfigured. options.staffOnly: true: enforced bydevCommandValidator.js; the member must have one ofconfig.moderation.staffRoles.options.nsfw: true: enforced bydevCommandValidator.js; guild channel interactions must run in an NSFW channel.testMode: true: command must run inconfig.handler.guildId.userPermissions: member must have each listed Discord permission.botPermissions: the bot member must have each listed Discord permission.- Component validators also prevent users from interacting with another user’s command-owned button or select menu when
interaction.message.interactionis present.
- The Guild slash-command handler in
src/events/Guild/interactionCreate.jssupportscommand.options.cooldownas a millisecond duration. The cooldown store is an in-memoryMapkeyed by Discord user ID, with command names as values, so it is per-process and clears on restart. - Slash cooldowns are per user and per command name. A user can be cooling down for one slash command while using another command, and another user is not blocked by the first user’s cooldown.
- The slash cooldown is recorded before
command.run(client, interaction)executes. Expiry usessetTimeout; if another timer has already removed the user entry, the expiry handler no-ops instead of throwing. - The active validation path in
src/events/validations/chatInputCommandValidator.jscalls chat-input commands directly and does not apply the Guild handler cooldown map. Verify the event loader caveat below before depending onoptions.cooldownin production. - Prefix commands are executed through
src/events/Guild/messageCreate.jswithawait command.run(client, message, args), so async command failures are caught by that handler’stry/catchand logged throughlog(error, "err"). - Prefix command metadata can include
data.permissionsanddata.developers;data.cooldownis present on the prefix eval command but is not enforced bymessageCreate.js.
src/handlers/events.jsregisters each direct folder undersrc/eventsas an event name, exceptvalidations, which is remapped tointeractionCreate.- Files under
src/events/readyandsrc/events/validationsexport callable functions and match that loader. - Files under
src/events/Guildexport{ event, run }objects and the folder name would register asGuild. That shape does not match the current loader’s callable function contract or Discord event names such asmessageCreate, so verify runtime registration before relying on those handlers for prefix commands or component routing.
Command Deployment
There are two deployment paths:- Ready-time slash registration in
src/events/ready/registerCommands.jsfetches guild application commands forprocess.env.DEV_GUILD_ID, compares local command data, and creates, edits, or deletes commands. - Ready-time context menu registration in
src/events/ready/registerContextMenus.jsalso usesprocess.env.DEV_GUILD_ID, but it only creates missing menus and deletes menus markeddeleted; it does not edit existing menu definitions. - Developer command deployment in
src/handlers/deploy.jswrites commands fromclient.collection.developercommandstoconfig.handler.guildId. It is invoked after login inExtendedClient.start()withoutawait, and can also be triggered by the developer-only/deploycommand.
- If slash commands do not update, confirm
DEV_GUILD_IDis set and the bot is in that guild. - If developer commands are missing, confirm
config.handler.guildIdresolves to the intended guild andconfig.moderation.developerscontains the operator’s Discord user ID. - If command options are not changing, inspect
src/utils/commandComparing.js; it normalizes name, description, options, and choices before deciding whether to edit an existing command. config.handler.deployandconfig.handler.guildDeployexist insrc/example.config.js, but the verified deployment paths above do not currently read those flags.
GitHub Release Automation
.github/workflows/release.yml publishes GitHub Releases from main when package.json changes the top-level version field.
Release workflow behavior:
- A push to
mainstarts theReleaseworkflow. - The workflow reads
package.jsonwith Node and derives the release tag asv<version>, for examplev1.2.1. - It checks only the latest pushed commit range,
HEAD~1..HEAD, for apackage.jsonline containing"version". - If the version changed, it fetches tags and skips the release when the derived tag already exists.
- If the tag is new, it sets up Node.js
22, installs dependencies withnpm ci || npm install, runsnpm test, generates a changelog from commits since the most recent version-sorted tag, and creates a non-draft, non-prerelease GitHub Release withsoftprops/action-gh-release.
- Bump
package.jsonin the commit that lands onmainwhen you want a release. The workflow does not create or commit version bumps. - The workflow creates a Git tag through the GitHub Release action; do not pre-create the same
v<version>tag unless you intend the workflow to skip release creation. - The workflow publishes a GitHub Release only. It does not publish an npm package, build Docker images, deploy the bot, or update Discord commands.
contents: writepermission is required so the workflow token can create the release and tag.- Release creation is gated by
npm test; keep tests passing before merging a version bump.
Economy Notes
Economy data is stored in MongoDB through theEcoSchema Prisma model. src/schemas/EcoSchema.js exports the prisma.ecoSchema delegate, and the model maps to the ecoschemas collection:
src/commands/slash/Economy/**:
/economy: creates an account withWallet: 0andBank: 1000, or deletes the found account withecoSchema.delete({ where: { id: doc.id } })./bal: reports wallet, bank, and total balances./deposit amount: moves money from wallet to bank.amountcan be a number orall./withdraw amount: moves money from bank to wallet.amountcan be a number orall./beg: randomly chooses a positive or negative wallet change. It updatesWalletonly if the user has an account, but still sends the result reply when no account exists./rob user: requires both users to have economy accounts and the robber to have at least$100, and it takes a per-user cooldown lock before database reads. The regression tests document the intended success/failure transfer behavior; verifyrob.jsdirectly before changing runtime behavior because several past bugs involved cooldown races and uncapped fines.
/rob workflow and constraints:
- The command checks an in-memory per-user cooldown before reading from MongoDB.
- It immediately records the robber’s user ID in
timeoutbefore the firstawait; this is the command’s only guard against concurrent calls from the same user. - It rejects self-robs, missing robber or target accounts, robber wallets below
$100, and target wallets below$100. - It rolls a 1-100 chance. Values up to
50are successful robberies. - The transfer or fine amount is rolled from
1..TargetData.Wallet. - On success, the robber gains the amount and the target loses it.
- On failure, the robber pays the target
Math.min(amount, Data.Wallet)so a fine cannot make the robber wallet negative. - After a saved success or failure, the cooldown is released after 60 seconds. Early validation failures and thrown errors release it immediately.
- Keep
try/catcharound all awaited work after the cooldown lock, or unexpected errors can leave the user stuck on cooldown. - Do not move
timeout.push(user.id)below anawait; that reintroduces the race covered bytests/rob-cooldown-race.test.js. - Because
EcoSchemahas no minimum-value validation, command code must prevent negative balances before callingecoSchema.update(). - Because there is no unique Prisma constraint for
{ Guild, User }, account creation still relies on application-levelfindFirstchecks. - Run
node --check src/commands/slash/Economy/rob.jsornpm testafter editing this module; a previous bad merge left invalid JavaScript that broke command loading during startup.
tests/ document important economy invariants:
tests/economy-amount-all.test.js:allmust match case-insensitively for deposit and withdraw.tests/economy-account-delete.test.js: documents the old deletion failure mode where code relied on document-shapeddeleteMany()behavior. Current Prisma code should delete or update through the model delegate.tests/rob-syntax.test.js:/robmust parse as valid JavaScript before the command loader requires it.tests/rob-cooldown-race.test.js:/robmust take its per-user cooldown lock before anyawaitto avoid overlapping balance saves.tests/rob-caught-penalty.test.js,tests/rob-failure-penalty.test.js, andtests/rob-fine-cap.test.js: a failed robbery fine must not exceed the robber’s current wallet.
node:test regression tests in tests/ before adjusting command behavior.
Operational Pitfalls
- The bot requires Discord gateway intents that match the enabled features.
ExtendedClientcurrently passes a numeric intent bitfield, so keep Discord Developer Portal settings in sync when changing message, member, or guild-dependent behavior. - Prisma/MongoDB connection failures are logged and rethrown from
src/handlers/prisma.js;ExtendedClient.start()attaches a.catch()and does not block Discord login while the connection attempt runs. Commands that query MongoDB still depend on a valid runtime URI, network, generated Prisma client, and database credentials. - Top.gg autoposting only starts when
TOPGG_TOKENis present, but the functions module is required during client startup. - The health endpoint is not authenticated. Do not expose port
8080publicly unless that is intentional for the hosting environment. - Prefix command support depends on the
messageCreatehandler insrc/events/Guild/messageCreate.js; because of the event loader caveat above, verify runtime registration before documenting prefix commands as available to server members.