ZapBot is an event-driven, plugin-based WhatsApp bot framework for Node.js, built on top of zapo-js. There is no commandHandler(), no switch(command) and no static prefix table anywhere in the codebase: every feature is a listener on an internal event bus and every command is a Map entry a plugin registers when it loads.
IMPORTANT: ZapBot is a framework provided for educational and personal use. If you build and run a bot with it, please:
- Use responsibly: Don't spam, harass or send unwanted messages to users.
- Respect privacy: Handle user data with care.
- No commercial use Without proper authorization.
- Legal compliance: Ensure your use complies with local laws, regulations and WhatsApp's own terms of service.
The maintainers are not responsible for any misuse of bots built with ZapBot or any consequences arising from their use.
Caution
WhatsApp may suspend or ban accounts that violate their terms of service. Use at your own risk.
- Plugin-based, zero-config: Drop a single
.jsfile intosrc/plugins/<category>/and it's live. Noplugin.jsonand no core file ever needs to change just to add a command. - Autoloaders for both plugins and events:
src/core/PluginManager.jsandsrc/events/index.js#loadEventsrecursively scan their directories and register whatever they find. - Event-driven:
message,command,media,image,video,gif,sticker,connection,readyand any custom event a plugin wants to emit, all on one bus (ctx.events). parseMessagebuilt in: Every incoming message is normalized once intotext,isMedia,isType(kind),isGroup,quoted,mentionsand boundsend/replyhelpers.- Middleware support: Koa-style
bot.use(async (ctx, next) => { ... await next() })for logging, anti-spam, permissions. - Multi-prefix, hot-reloadable:
.,!,#by default; change the prefix list at runtime viactx.config, no restart needed. - Automatic sticker conversion: Opt-in event listener that turns incoming images/GIFs/short videos into stickers.
- Default plugins:
ping,menu(reads the live plugin/command list, grouped by category, never hardcoded),sticker. - No build step: Plain modern JavaScript (ESM), one JSDoc type file at the project root (
types.js), no.d.ts, notsconfig/jsconfig, no compiler. Module aliases come from Node's nativepackage.json#imports.
- Node.js >= 20.9.0 (required by
zapo-js). - ffmpeg on
PATH: required bywa-sticker-formatterto convert video/GIF input into stickers. Not needed if you only send/convert static images. - Git, to clone the repository.
- A terminal that can render QR codes or a phone ready to enter an 8-character pairing code instead.
Verify installation:
node --version
npm --version
git --versionSee docs/installation.md for full details, including a Windows-specific native-module note for better-sqlite3/sharp.
git clone https://github.com/DikaArdnt/zapbot.git
cd zapbotnpm installEverything lives in config/config.js with sane defaults; no environment variable is required to get started. Copy .env.example to .env (git-ignored) if you want to override settings like session.phoneNumber or session.pairing without editing the config file directly.
npm start
npm run dev- QR pairing (default): A QR code prints in your terminal. Open WhatsApp → Linked devices → Link a device and scan it.
- Pairing code: Set
session.pairing: 'code'andsession.phoneNumberinconfig/config.js, then start the bot; an 8-character code prints in the terminal.
Once paired, credentials are persisted to session.storePath (default ./.auth/state.sqlite), so subsequent npm start runs reconnect automatically.
ZapBot reads its configuration from config/config.js, where every field can be overridden by an environment variable (loaded via dotenv):
| Field | Env variable | Description | Default |
|---|---|---|---|
prefix |
PREFIX |
Comma-separated list of command prefixes | ['.', '!', '#'] |
session.id |
SESSION_ID |
Logical session identifier forwarded to WaClient |
default |
session.storePath |
SESSION_STORE_PATH |
Where the SQLite auth/session store is persisted | ./.auth/state.sqlite |
session.deviceBrowser |
SESSION_DEVICE_BROWSER |
Device browser name forwarded to WaClient |
edge |
session.pairing |
SESSION_PAIRING |
'qr' or 'code' |
qr |
session.phoneNumber |
SESSION_PHONE_NUMBER |
Required when pairing is 'code' (digits + country code) |
(empty) |
session.autoReconnect |
SESSION_AUTO_RECONNECT |
Reconnect automatically after a non-logout disconnect | true |
session.maxReconnectAttempts |
SESSION_MAX_RECONNECT_ATTEMPTS |
Maximum reconnect attempts before giving up | 10 |
session.markOnlineOnConnect |
SESSION_MARK_ONLINE_ON_CONNECT |
Whether to mark the account as online on connect | false |
autoSticker.enabled |
AUTO_STICKER_ENABLED |
Master switch for automatic sticker conversion | false |
autoSticker.videoDurationLimit |
AUTO_STICKER_VIDEO_DURATION_LIMIT |
Videos longer than this (seconds) are ignored, not converted | 10 |
autoSticker.packname |
AUTO_STICKER_PACKNAME |
Sticker pack name embedded in WebP EXIF metadata | Bot Sticker |
autoSticker.author |
AUTO_STICKER_AUTHOR |
Sticker author embedded in WebP EXIF metadata | Developer |
plugins.directory |
PLUGINS_DIRECTORY |
Directory recursively scanned for plugin files | ./src/plugins |
logger.level |
LOG_LEVEL |
'trace' | 'debug' | 'info' | 'warn' | 'error' |
info |
logger.pretty |
LOG_PRETTY |
Pretty-print logs to the console (requires pino-pretty) |
true |
logger.name |
LOG_NAME |
Name of the logger included in each log line | ZapBot |
Full reference: docs/configuration.md.
config/config.js # the only config file (prefix, session, autoSticker, plugins, logger)
types.js # single root JSDoc type file (#types alias)
eslint.config.js # ESLint flat config (import-x order rules + prettier integration)
.prettierrc.json # Prettier config (LF line endings, no semicolons, single quotes)
src/
├── core/ # Bot, ConfigManager, EventManager, MiddlewareManager,
│ # CommandManager, PluginManager, ClientWrapper, Logger
├── events/ # autoloaded: message.js, media.js, stickerConverter.js, connection.js, index.js (loader)
├── plugins/
│ ├── main/ # ping.js, menu.js
│ └── media/ # sticker.js
├── utils/ # helper.js, media.js, sticker.js, parseMessage.js, loader.js
└── index.js # boots Bot with config/config.js
| Path | Purpose |
|---|---|
| src/core | The framework itself: Bot, ConfigManager, EventManager, MiddlewareManager, CommandManager, PluginManager, ClientWrapper, Logger |
| src/events | Built-in event modules, autoloaded at boot by src/events/index.js#loadEvents |
| src/plugins | User-facing features, one file per plugin, autoloaded by PluginManager |
| src/utils | Pure, stateless helpers shared by core/events/plugins |
| config/config.js | The single source of runtime configuration |
| types.js | Single root JSDoc type file, referenced via import('#types').SomeType |
There's no bundler and no jsconfig.json/tsconfig.json. Aliases come from package.json's native "imports" field:
"imports": {
"#types": "./types.js",
"#config": "./config/config.js",
"#core/*.js": "./src/core/*.js",
"#events/*.js": "./src/events/*.js",
"#plugins/*.js": "./src/plugins/*.js",
"#utils/*.js": "./src/utils/*.js"
}Always import through these aliases, never relative ../../ chains.
A plugin is a single file, src/plugins/<category>/<name>.js, default-exporting a BotPlugin (see types.js):
/* src/plugins/main/hello.js */
/** @type {import('#types').BotPlugin} */
export default {
name: 'hello',
type: 'command',
commands: ['hello'],
description: 'Say hello',
init(ctx) {
ctx.logger.debug('hello plugin initialized');
},
async execute(command) {
await command.reply(`Hello, ${command.pushName ?? 'there'}!`);
},
};That's it: .hello works and shows up in .menu under the main category automatically. No metadata file, no registration step. More in docs/plugin-development.md.
/* src/events/myFeature.js */
/** @type {import('#types').BotEventModule} */
export default {
name: 'autoStickerOnImage',
register(ctx) {
ctx.events.on('image', async mediaContext => {
if (!ctx.config.get('autoSticker.enabled')) return;
const bytes = await mediaContext.downloadBytes();
const sticker = await ctx.utils.sticker.createSticker(Buffer.from(bytes));
await ctx.wa.reply(mediaContext.raw, {
type: 'sticker',
media: sticker,
mimetype: 'image/webp',
});
});
},
};Drop it in src/events/ and it's autoloaded, no src/events/index.js edit required. More in docs/event-development.md.
The plugin loader recursively scans src/plugins. Currently shipped:
main:ping,menu(lists every loaded plugin/command, grouped by category, generated live rather than hardcoded)media:sticker
ESLint (with eslint-plugin-import-x for import ordering) and Prettier (LF line endings enforced via .prettierrc.json/.editorconfig/.gitattributes):
npm run lint # check
npm run lint:fix # check + autofix (also sorts/groups imports: node: -> packages -> #alias -> relative)
npm run format # prettier --write
npm run format:check # prettier --check, no writesYou'll know the bot is running when you see, in order (from src/core/Bot.js, src/core/ClientWrapper.js and src/events/connection.js):
Command prefixes: . ! #andRegistered commands: ping, menu, sticker(or your own) logged at startup.- Either
Scan the QR code below within <N>swith a rendered QR code orPairing code: XXXX-XXXX, depending onsession.pairing. Paired as <jid> (<pushName>)once WhatsApp accepts the pairing.Connected (new login: true/false)andBot is ready — <N> plugin(s) loaded.- The bot replying to
.ping,.menuor.sticker(reply to an image) in a chat.
WhatsApp connection drops
ClientWrapper.scheduleReconnect()retries with exponential backoff (1000 * 2^attempt, capped at 30s) up tosession.maxReconnectAttempts(default10), only whensession.autoReconnectistrue.- After the max attempts is reached, it logs
Giving up reconnecting after <N> attempts.and stops; there is no further automatic recovery. - If the session is logged out,
ClientWrapperlogs the specific reason (FAILURE_LOCKED,FAILURE_BANNED,FAILURE_CLIENT_TOO_OLD,FAILURE_SERVICE_UNAVAILABLE,STREAM_ERROR_DEVICE_REMOVEDor a generic re-pairing message) and you'll need to re-pair by restarting the bot.
Plugin or command errors
CommandManager.dispatch()wraps everyplugin.execute()call intry/catch; a thrown error is logged (Plugin "<name>" threw while executing "<command>") but does not crash the bot or the message loop.PluginManager.registerPlugin()skips (with a warning) any plugin missingname/init, anytype: 'command'plugin missingcommands[]orexecute()and any plugin withenabled: false.
Auth/session issues
- Session state is persisted to
session.storePath(default./.auth/state.sqlite) via@zapo-js/store-sqlite. If pairing seems stuck or corrupted, stop the bot and delete that file to force a fresh pairing.
Native module install failures (better-sqlite3, sharp)
- See the Windows native modules note: switching to a current Node LTS with prebuilt binaries is the fastest fix.
This project has no automated test suite. Changes are verified manually:
- Start the bot with
npm start(ornpm run devto auto-restart on file changes). - Pair a WhatsApp account (QR or code, see Quick Start).
- Trigger the relevant command or event (e.g. send
.ping,.menuor.stickerin a chat or reproduce the event condition your change affects). - Confirm the expected reply/log output, per the Verification section above.
Before opening a pull request, also run:
npm run lint
npm run format:check- docs/installation.md
- docs/configuration.md
- docs/plugin-development.md
- docs/event-development.md
- docs/api-reference.md
- docs/examples.md
- AGENTS.md: Coding rules and architecture guide for contributors (human or AI).
- CONTRIBUTING.md: How to fork, branch and submit a pull request.
See CONTRIBUTING.md for the full workflow: forking, coding rules, commit message conventions (Conventional Commits) and how to open a pull request against DikaArdnt/zapbot.