feat(framework): implement enterprise bot framework - #2710
Conversation
|
Thanks for opening this pull request and contributing to the project! The next step is for the maintainers to review your changes. If everything looks good, it will be approved and merged into the main branch. In the meantime, anyone in the community is encouraged to test this pull request and provide feedback. ✅ How to confirm it worksIf you’ve tested this PR, please comment below with: This helps us speed up the review and merge process. 📦 To test this PR locally:If you encounter any issues or have feedback, feel free to comment as well. |
📝 WalkthroughWalkthroughAdds a new SQLite-backed bot framework with middleware routing, reconnection and message queuing, session and group statistics management, media conversion helpers, public exports, documentation, and a sticker-command example. ChangesEnterprise bot framework
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant WhatsApp
participant Bot
participant Context
participant MediaManager
participant SQLiteStore
WhatsApp->>Bot: messages.upsert
Bot->>Context: create message context
Bot->>SQLiteStore: record session or group statistics
Bot->>Context: execute registered middleware
Context->>MediaManager: convert media when needed
MediaManager-->>Context: return converted buffer
Context->>Bot: send quoted reply
Bot->>WhatsApp: sendMessage
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Example/bot-example.ts`:
- Around line 50-53: Move the `creds.update` listener registration in the bot
startup flow to after `await bot.start()`, when `bot.socket` exists. Update the
code around `saveCreds` so credential updates are consistently persisted, while
preserving the existing handler.
In `@src/Framework/Bot.ts`:
- Around line 115-128: Move the analytics block calling stats.observeMessage
inside the try boundary that currently protects executeMiddlewares in the
message handler. Ensure synchronous SQLite failures are caught and reported by
the existing middleware error handling while preserving the current participant,
group, and sticker conditions.
- Around line 59-79: Add unit tests covering the public routing APIs use,
command, and onText, including middleware ordering, duplicate next() behavior,
and command-boundary matching; also test queue resolution/rejection and socket
reconnect decisions. Add integration tests for the new incoming event handlers
and protocol/stanza paths introduced in the related changes.
- Around line 59-175: For src/Framework/Bot.ts lines 59-175, add unit and
integration tests covering middleware routing, command boundary matching, queued
sendMessage behavior and draining, statistics observation, and reconnect
cleanup. For src/Framework/StatsManager.ts lines 61-107, add tests for persisted
counters, rankings, JID normalization, and ghost detection. For
src/Framework/Context.ts lines 19-65, add tests covering message-text variants,
sessions, replies, reactions, stickers, and voice notes, using protocol/event
integration coverage where applicable.
- Around line 81-88: Replace console-based diagnostics with the configured
pino-compatible logger across Bot.sendMessage, queue-drain handling, and
middleware/connection handling in src/Framework/Bot.ts (81-88, 93-103, 124-159),
logging queued-message metadata without interpolating JIDs, using objects first
and messages last, and reporting middleware failures under err. Apply the same
logger contract in Example/bot-example.ts (44-54) by replacing its console
calls; preserve the existing diagnostic events and connection fields as
structured data.
- Around line 81-90: Update Bot.sendMessage and the loggedOut handling around
the referenced reconnection logic to reject every queued message when
reconnection is terminal, then clear messageQueue so payloads and promises are
released. Ensure subsequent queued sends do not remain pending after loggedOut;
add a bounded queue or enqueue timeout only if supported by the existing queue
design.
- Around line 63-69: Update the command matching in Bot.command so
startsWith(cmd) only matches an exact command or when the character immediately
after cmd is whitespace. Preserve handler and next execution for valid matches,
while preventing longer command names such as !stickerSpam from matching
!sticker.
- Around line 107-108: Update Framework.Bot.start to prevent duplicate socket
creation: retain and reuse the active socket when appropriate, cancel and clear
any pending reconnect timer before replacing it, and detach listeners from the
previous socket during teardown or restart. Ensure reconnect scheduling stores
its timer handle so restart and cleanup can cancel it.
- Around line 18-24: The framework surface still exposes unsafe any types. In
src/Framework/Bot.ts lines 18-24 and 81, derive EnqueuedMessage’s resolve
callback and sendMessage’s return type from WASocket['sendMessage']; in
src/Framework/Context.ts lines 25-31, default session generics to unknown; and
in Example/bot-example.ts lines 31-38, replace the any cast with a typed
WAMessage/quoted-message shape.
In `@src/Framework/Context.ts`:
- Around line 19-23: Update the Context.text getter to fall back to captions on
supported media messages, including image and video captions, after checking
conversation and extended text. Preserve the existing undefined result when no
text or media caption is present so Bot.command can receive direct media
commands.
In `@src/Framework/MediaManager.ts`:
- Around line 28-32: Replace synchronous filesystem calls throughout
MediaManager with awaited promise-based operations: at
src/Framework/MediaManager.ts lines 28-32 and 86-90, use fs.promises.writeFile
or copyFile; at lines 75-79 and 103-107, use fs.promises.readFile and replace
existence checks plus unlinkSync with fs.promises.unlink(path).catch(() => {}).
Ensure the containing methods remain async and preserve existing cleanup and
data flow.
In `@src/Framework/SessionManager.ts`:
- Line 14: Replace the default any types in SessionManager.get and the other
affected SessionManager methods with unknown, and update both MediaManager
error-handler parameters to use Error or unknown. Apply these changes at
src/Framework/SessionManager.ts lines 14-14, 18-18, and 26-26, plus
src/Framework/MediaManager.ts lines 51-51 and 100-100, without changing the
surrounding behavior.
In `@src/Framework/StatsManager.ts`:
- Around line 61-66: Normalize participant JIDs to a canonical user key in
observeMessage before insertStmt.run, and apply the same normalization to
participant IDs used by getGhosts when building or querying its Map. Replace
raw-string matching with the shared canonicalization utility so PN/LID/device
variants resolve to the same stats entry and active members are not classified
as ghosts.
In `@src/Framework/Store/SQLiteStore.ts`:
- Around line 28-43: Update SQLiteStore.set so it always JSON.stringify values,
including strings, before passing them to setStmt.run. Remove the typeof value
=== 'string' special case, preserving get’s JSON.parse-based round-trip behavior
and string types.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 97921d22-d208-4758-9423-4ada1a3b6606
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (11)
Example/bot-example.tsREADME.mdpackage.jsonsrc/Framework/Bot.tssrc/Framework/Context.tssrc/Framework/MediaManager.tssrc/Framework/SessionManager.tssrc/Framework/StatsManager.tssrc/Framework/Store/SQLiteStore.tssrc/Framework/index.tssrc/index.ts
| // Guardar credenciales al conectarse/actualizarse | ||
| bot.socket?.ev.on('creds.update', saveCreds) | ||
|
|
||
| await bot.start() |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Register creds.update after the socket exists.
bot.socket is always undefined before bot.start(), so optional chaining silently skips this handler and credential updates are never persisted.
- bot.socket?.ev.on('creds.update', saveCreds)
-
await bot.start()
+ bot.socket?.ev.on('creds.update', saveCreds)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Guardar credenciales al conectarse/actualizarse | |
| bot.socket?.ev.on('creds.update', saveCreds) | |
| await bot.start() | |
| // Guardar credenciales al conectarse/actualizarse | |
| await bot.start() | |
| bot.socket?.ev.on('creds.update', saveCreds) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Example/bot-example.ts` around lines 50 - 53, Move the `creds.update`
listener registration in the bot startup flow to after `await bot.start()`, when
`bot.socket` exists. Update the code around `saveCreds` so credential updates
are consistently persisted, while preserving the existing handler.
| type EnqueuedMessage = { | ||
| jid: string | ||
| content: AnyMessageContent | ||
| options: MiscMessageGenerationOptions | ||
| resolve: (value: any) => void | ||
| reject: (reason?: any) => void | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== Bot.ts outline ==\n'
ast-grep outline src/Framework/Bot.ts --view expanded || true
printf '\n== Context.ts outline ==\n'
ast-grep outline src/Framework/Context.ts --view expanded || true
printf '\n== Example/bot-example.ts outline ==\n'
ast-grep outline Example/bot-example.ts --view expanded || true
printf '\n== Relevant lines in Bot.ts ==\n'
nl -ba src/Framework/Bot.ts | sed -n '1,180p'
printf '\n== Relevant lines in Context.ts ==\n'
nl -ba src/Framework/Context.ts | sed -n '1,120p'
printf '\n== Relevant lines in Example/bot-example.ts ==\n'
nl -ba Example/bot-example.ts | sed -n '1,120p'
printf '\n== Search for sendMessage typings / downloadMediaMessage usage ==\n'
rg -n "sendMessage\\(|downloadMediaMessage|WASocket" src Example node_modules --glob '!**/dist/**' --glob '!**/build/**' || trueRepository: WhiskeySockets/Baileys
Length of output: 2231
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Bot.ts\n'
nl -ba src/Framework/Bot.ts | sed -n '1,130p'
printf '\nContext.ts\n'
nl -ba src/Framework/Context.ts | sed -n '1,80p'
printf '\nExample/bot-example.ts\n'
nl -ba Example/bot-example.ts | sed -n '1,70p'Repository: WhiskeySockets/Baileys
Length of output: 207
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== Bot.ts ==\n'
sed -n '1,140p' src/Framework/Bot.ts | cat -n
printf '\n== Context.ts ==\n'
sed -n '1,90p' src/Framework/Context.ts | cat -n
printf '\n== Example/bot-example.ts ==\n'
sed -n '1,90p' Example/bot-example.ts | cat -n
printf '\n== Search for sendMessage / downloadMediaMessage type definitions ==\n'
rg -n "sendMessage\\s*[:(]|downloadMediaMessage|type .*WASocket|interface .*WASocket|export type WASocket" src . --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' || trueRepository: WhiskeySockets/Baileys
Length of output: 25162
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== Socket sendMessage signature ==\n'
sed -n '1310,1355p' src/Socket/messages-send.ts | cat -n
printf '\n== downloadMediaMessage signature ==\n'
sed -n '1040,1085p' src/Utils/messages.ts | cat -n
printf '\n== index exports for Context/Bot/Media helpers ==\n'
sed -n '1,80p' src/index.ts | cat -nRepository: WhiskeySockets/Baileys
Length of output: 4032
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== SessionManager signatures ==\n'
sed -n '1,220p' src/Framework/SessionManager.ts | cat -n
printf '\n== Search for SessionManager get/set/update defaults ==\n'
rg -n "class SessionManager|get<|set<|update<|delete\\(" src/Framework src --glob '!**/dist/**' --glob '!**/build/**' || trueRepository: WhiskeySockets/Baileys
Length of output: 5791
Remove the remaining any types from the framework surface.
src/Framework/Bot.ts: derive the queue callback andsendMessagereturn type fromWASocket['sendMessage'].src/Framework/SessionManager.tsandsrc/Framework/Context.ts: default the session generics tounknowninstead ofany.Example/bot-example.ts: replace theas anycast with a typedWAMessage/quoted-message shape.
📍 Affects 3 files
src/Framework/Bot.ts#L18-L24(this comment)src/Framework/Bot.ts#L81-L81src/Framework/Context.ts#L25-L31Example/bot-example.ts#L31-L38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Framework/Bot.ts` around lines 18 - 24, The framework surface still
exposes unsafe any types. In src/Framework/Bot.ts lines 18-24 and 81, derive
EnqueuedMessage’s resolve callback and sendMessage’s return type from
WASocket['sendMessage']; in src/Framework/Context.ts lines 25-31, default
session generics to unknown; and in Example/bot-example.ts lines 31-38, replace
the any cast with a typed WAMessage/quoted-message shape.
Source: Coding guidelines
| public use(middleware: MiddlewareFn) { | ||
| this.middlewares.push(middleware) | ||
| } | ||
|
|
||
| public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) { | ||
| this.use(async (ctx, next) => { | ||
| if (ctx.text?.startsWith(cmd)) { | ||
| await handler(ctx) | ||
| } | ||
| await next() | ||
| }) | ||
| } | ||
|
|
||
| public onText(handler: (ctx: Context) => Promise<void> | void) { | ||
| this.use(async (ctx, next) => { | ||
| if (ctx.text) { | ||
| await handler(ctx) | ||
| } | ||
| await next() | ||
| }) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Add tests for routing, queuing, and socket lifecycle paths.
Unit-test middleware ordering/duplicate next(), command boundaries, queue resolution/rejection, and reconnect decisions. Add integration tests for the new incoming event handlers.
As per coding guidelines, new public APIs require unit tests and new protocol paths or stanza handlers require integration tests.
Also applies to: 107-175
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Framework/Bot.ts` around lines 59 - 79, Add unit tests covering the
public routing APIs use, command, and onText, including middleware ordering,
duplicate next() behavior, and command-boundary matching; also test queue
resolution/rejection and socket reconnect decisions. Add integration tests for
the new incoming event handlers and protocol/stanza paths introduced in the
related changes.
Source: Coding guidelines
| public use(middleware: MiddlewareFn) { | ||
| this.middlewares.push(middleware) | ||
| } | ||
|
|
||
| public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) { | ||
| this.use(async (ctx, next) => { | ||
| if (ctx.text?.startsWith(cmd)) { | ||
| await handler(ctx) | ||
| } | ||
| await next() | ||
| }) | ||
| } | ||
|
|
||
| public onText(handler: (ctx: Context) => Promise<void> | void) { | ||
| this.use(async (ctx, next) => { | ||
| if (ctx.text) { | ||
| await handler(ctx) | ||
| } | ||
| await next() | ||
| }) | ||
| } | ||
|
|
||
| public async sendMessage(jid: string, content: AnyMessageContent, options: MiscMessageGenerationOptions = {}): Promise<any> { | ||
| if (this.isConnected && this.socket) { | ||
| return this.socket.sendMessage(jid, content, options) | ||
| } else { | ||
| // Queue the message | ||
| return new Promise((resolve, reject) => { | ||
| this.messageQueue.push({ jid, content, options, resolve, reject }) | ||
| console.log(`[Bot] Socket no conectado. Mensaje para ${jid} encolado. (Cola: ${this.messageQueue.length})`) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| private drainQueue() { | ||
| if (!this.isConnected || !this.socket || this.messageQueue.length === 0) return | ||
|
|
||
| console.log(`[Bot] Drenando cola de mensajes: ${this.messageQueue.length} pendientes.`) | ||
| const queueToProcess = [...this.messageQueue] | ||
| this.messageQueue = [] | ||
|
|
||
| for (const msg of queueToProcess) { | ||
| this.socket.sendMessage(msg.jid, msg.content, msg.options) | ||
| .then(msg.resolve) | ||
| .catch(msg.reject) | ||
| } | ||
| } | ||
|
|
||
| public async start() { | ||
| this.socket = makeWASocket(this.config) | ||
|
|
||
| this.socket.ev.on('messages.upsert', async ({ messages, type }) => { | ||
| if (type !== 'notify') return | ||
| for (const msg of messages) { | ||
| const ctx = new Context(this, msg) | ||
|
|
||
| // Analíticas: Observar el mensaje antes de los middlewares | ||
| if (this.stats && ctx.remoteJid && isJidGroup(ctx.remoteJid)) { | ||
| const participant = msg.key.participant || msg.participant | ||
| if (participant) { | ||
| const isSticker = !!msg.message?.stickerMessage | ||
| this.stats.observeMessage(ctx.remoteJid, participant, isSticker) | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| await this.executeMiddlewares(ctx) | ||
| } catch (err) { | ||
| console.error('Error executing middleware:', err) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| this.socket.ev.on('connection.update', (update) => { | ||
| const { connection, lastDisconnect } = update | ||
|
|
||
| if (connection === 'open') { | ||
| console.log('[Bot] Conectado exitosamente.') | ||
| this.isConnected = true | ||
| this.reconnectAttempts = 0 | ||
| this.drainQueue() | ||
| } | ||
|
|
||
| if (connection === 'close') { | ||
| this.isConnected = false | ||
| const error = lastDisconnect?.error as Boom | undefined | ||
| const statusCode = error?.output?.statusCode | ||
| const shouldReconnect = statusCode !== DisconnectReason.loggedOut | ||
|
|
||
| console.log(`[Bot] Conexión cerrada. Razón: ${statusCode}`) | ||
|
|
||
| if (shouldReconnect) { | ||
| this.reconnectAttempts++ | ||
| const delay = Math.min(this.MAX_RECONNECT_DELAY, this.BASE_RECONNECT_DELAY * (2 ** (this.reconnectAttempts - 1))) | ||
| console.log(`[Bot] Intentando reconectar en ${delay}ms (Intento ${this.reconnectAttempts})...`) | ||
|
|
||
| setTimeout(() => { | ||
| this.start() | ||
| }, delay) | ||
| } else { | ||
| console.log('[Bot] Sesión cerrada (LoggedOut). No se reconectará automáticamente.') | ||
| } | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| private async executeMiddlewares(ctx: Context) { | ||
| let index = -1 | ||
| const dispatch = async (i: number): Promise<void> => { | ||
| if (i <= index) throw new Error('next() called multiple times') | ||
| index = i | ||
| const middleware = this.middlewares[i] | ||
| if (middleware) { | ||
| await middleware(ctx, () => dispatch(i + 1)) | ||
| } | ||
| } | ||
| await dispatch(0) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
The new public framework and event paths lack required test coverage.
src/Framework/Bot.ts#L59-L175: test middleware routing, command boundaries, queue behavior, statistics wiring, and reconnect cleanup.src/Framework/StatsManager.ts#L61-L107: test persisted counters, rankings, JID normalization, and ghost detection.src/Framework/Context.ts#L19-L65: test message-text variants, sessions, replies, reactions, stickers, and voice notes.
As per coding guidelines, public APIs require unit tests and protocol/event paths require integration tests.
📍 Affects 3 files
src/Framework/Bot.ts#L59-L175(this comment)src/Framework/StatsManager.ts#L61-L107src/Framework/Context.ts#L19-L65
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Framework/Bot.ts` around lines 59 - 175, For src/Framework/Bot.ts lines
59-175, add unit and integration tests covering middleware routing, command
boundary matching, queued sendMessage behavior and draining, statistics
observation, and reconnect cleanup. For src/Framework/StatsManager.ts lines
61-107, add tests for persisted counters, rankings, JID normalization, and ghost
detection. For src/Framework/Context.ts lines 19-65, add tests covering
message-text variants, sessions, replies, reactions, stickers, and voice notes,
using protocol/event integration coverage where applicable.
Source: Coding guidelines
| public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) { | ||
| this.use(async (ctx, next) => { | ||
| if (ctx.text?.startsWith(cmd)) { | ||
| await handler(ctx) | ||
| } | ||
| await next() | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Require a command boundary after the prefix.
startsWith(cmd) also invokes !sticker for inputs such as !stickerSpam. Match the exact command or require whitespace after it.
- if (ctx.text?.startsWith(cmd)) {
+ if (ctx.text === cmd || ctx.text?.startsWith(`${cmd} `)) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) { | |
| this.use(async (ctx, next) => { | |
| if (ctx.text?.startsWith(cmd)) { | |
| await handler(ctx) | |
| } | |
| await next() | |
| }) | |
| public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) { | |
| this.use(async (ctx, next) => { | |
| if (ctx.text === cmd || ctx.text?.startsWith(`${cmd} `)) { | |
| await handler(ctx) | |
| } | |
| await next() | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Framework/Bot.ts` around lines 63 - 69, Update the command matching in
Bot.command so startsWith(cmd) only matches an exact command or when the
character immediately after cmd is whitespace. Preserve handler and next
execution for valid matches, while preventing longer command names such as
!stickerSpam from matching !sticker.
| public get text(): string | undefined { | ||
| return this.message.message?.conversation || | ||
| this.message.message?.extendedTextMessage?.text || | ||
| undefined | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Include media captions in Context.text.
The sticker example expects !sticker on an image or video, but Bot.command only sees Context.text; direct media captions are currently ignored, so the handler never runs.
Proposed fix
public get text(): string | undefined {
- return this.message.message?.conversation ||
- this.message.message?.extendedTextMessage?.text ||
+ return this.message.message?.conversation ||
+ this.message.message?.extendedTextMessage?.text ||
+ this.message.message?.imageMessage?.caption ||
+ this.message.message?.videoMessage?.caption ||
undefined
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public get text(): string | undefined { | |
| return this.message.message?.conversation || | |
| this.message.message?.extendedTextMessage?.text || | |
| undefined | |
| } | |
| public get text(): string | undefined { | |
| return this.message.message?.conversation || | |
| this.message.message?.extendedTextMessage?.text || | |
| this.message.message?.imageMessage?.caption || | |
| this.message.message?.videoMessage?.caption || | |
| undefined | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Framework/Context.ts` around lines 19 - 23, Update the Context.text
getter to fall back to captions on supported media messages, including image and
video captions, after checking conversation and extended text. Preserve the
existing undefined result when no text or media caption is present so
Bot.command can receive direct media commands.
| if (Buffer.isBuffer(inputPathOrBuffer)) { | ||
| fs.writeFileSync(tempInput, inputPathOrBuffer) | ||
| } else { | ||
| fs.copyFileSync(inputPathOrBuffer, tempInput) | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔴 Critical | 🏗️ Heavy lift
Replace synchronous filesystem operations with async equivalents.
Using synchronous filesystem operations (fs.writeFileSync, fs.readFileSync, fs.copyFileSync, etc.) blocks the main Node.js event loop. In a high-scalability bot framework, this will delay the processing of concurrent messages and can cause the WebSocket connection to drop due to missed pings.
src/Framework/MediaManager.ts#L28-L32: Replacefs.writeFileSyncandfs.copyFileSyncwithawait fs.promises.writeFileandawait fs.promises.copyFile.src/Framework/MediaManager.ts#L75-L79: Replacefs.readFileSyncwithawait fs.promises.readFile. Replacefs.existsSyncandfs.unlinkSyncwithawait fs.promises.unlink(path).catch(() => {}).src/Framework/MediaManager.ts#L86-L90: Replacefs.writeFileSyncandfs.copyFileSyncwithawait fs.promises.writeFileandawait fs.promises.copyFile.src/Framework/MediaManager.ts#L103-L107: Replacefs.readFileSyncwithawait fs.promises.readFile. Replacefs.existsSyncandfs.unlinkSyncwithawait fs.promises.unlink(path).catch(() => {}).
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 28-28: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(tempInput, inputPathOrBuffer)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
📍 Affects 1 file
src/Framework/MediaManager.ts#L28-L32(this comment)src/Framework/MediaManager.ts#L75-L79src/Framework/MediaManager.ts#L86-L90src/Framework/MediaManager.ts#L103-L107
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Framework/MediaManager.ts` around lines 28 - 32, Replace synchronous
filesystem calls throughout MediaManager with awaited promise-based operations:
at src/Framework/MediaManager.ts lines 28-32 and 86-90, use
fs.promises.writeFile or copyFile; at lines 75-79 and 103-107, use
fs.promises.readFile and replace existence checks plus unlinkSync with
fs.promises.unlink(path).catch(() => {}). Ensure the containing methods remain
async and preserve existing cleanup and data flow.
| return `session_${jid}` | ||
| } | ||
|
|
||
| public get<T = any>(jid: string): T | undefined { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Avoid using any in new code.
As per coding guidelines, do not use any in new code. Use unknown or a more specific type to ensure type safety.
src/Framework/SessionManager.ts#L14-L14: ReplaceT = anywithT = unknown.src/Framework/SessionManager.ts#L18-L18: ReplaceT = anywithT = unknown.src/Framework/SessionManager.ts#L26-L26: ReplaceT = anywithT = unknown.src/Framework/MediaManager.ts#L51-L51: Replace(err: any)with(err: Error)or(err: unknown).src/Framework/MediaManager.ts#L100-L100: Replace(err: any)with(err: Error)or(err: unknown).
📍 Affects 2 files
src/Framework/SessionManager.ts#L14-L14(this comment)src/Framework/SessionManager.ts#L18-L18src/Framework/SessionManager.ts#L26-L26src/Framework/MediaManager.ts#L51-L51src/Framework/MediaManager.ts#L100-L100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Framework/SessionManager.ts` at line 14, Replace the default any types in
SessionManager.get and the other affected SessionManager methods with unknown,
and update both MediaManager error-handler parameters to use Error or unknown.
Apply these changes at src/Framework/SessionManager.ts lines 14-14, 18-18, and
26-26, plus src/Framework/MediaManager.ts lines 51-51 and 100-100, without
changing the surrounding behavior.
Source: Coding guidelines
| public observeMessage(groupJid: string, userJid: string, isSticker: boolean) { | ||
| const msgCount = 1 | ||
| const stickerCount = isSticker ? 1 : 0 | ||
| const now = Date.now() | ||
|
|
||
| this.insertStmt.run(groupJid, userJid, msgCount, stickerCount, now) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline src/Framework/StatsManager.ts --view expanded
printf '\n--- file ---\n'
cat -n src/Framework/StatsManager.ts | sed -n '1,220p'
printf '\n--- JID helper usage in this file ---\n'
rg -n "jidDecode|jidNormalizedUser|areJidsSameUser|isJidUser|split\\('@'\\)|===" src/Framework/StatsManager.ts
printf '\n--- related references ---\n'
rg -n "getGhosts|observeMessage|StatsManager" srcRepository: WhiskeySockets/Baileys
Length of output: 5301
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n--- call sites ---\n'
rg -n "observeMessage\\(|getGhosts\\(" src
printf '\n--- participant id handling ---\n'
rg -n "participants\\.map\\(|groupMetadata\\(|jidNormalizedUser|jidDecode|areJidsSameUser|isJidUser|participant\\.id|msg\\.key\\.participant|remoteJid" src
printf '\n--- StatsManager neighbors ---\n'
ast-grep outline src/Framework --view expandedRepository: WhiskeySockets/Baileys
Length of output: 33736
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n--- StatsManager references ---\n'
rg -n "observeMessage\\(|getGhosts\\(" src
printf '\n--- JID helper / metadata references ---\n'
rg -n "jidNormalizedUser|jidDecode|areJidsSameUser|isJidUser|groupMetadata\\(|participants\\.map\\(|participant\\.id|msg\\.key\\.participant|remoteJid" src
printf '\n--- file list in src/Framework ---\n'
git ls-files src/FrameworkRepository: WhiskeySockets/Baileys
Length of output: 29705
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n--- StatsManager call sites ---\n'
rg -n "observeMessage\\(|getGhosts\\(" src
printf '\n--- JID and metadata handling ---\n'
rg -n "jidNormalizedUser|jidDecode|areJidsSameUser|isJidUser|groupMetadata\\(|participants\\.map\\(|participant\\.id|msg\\.key\\.participant|remoteJid" src
printf '\n--- related Framework files ---\n'
git ls-files src/FrameworkRepository: WhiskeySockets/Baileys
Length of output: 29695
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n--- Bot.ts slice ---\n'
sed -n '100,130p' src/Framework/Bot.ts | cat -n
printf '\n--- process-message normalization slice ---\n'
sed -n '110,140p' src/Utils/process-message.ts | cat -n
printf '\n--- group metadata consumers around participant ids ---\n'
sed -n '90,120p' src/Socket/groups.ts | cat -n
sed -n '210,245p' src/Socket/groups.ts | cat -nRepository: WhiskeySockets/Baileys
Length of output: 5400
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n--- messages.upsert emission path ---\n'
rg -n "messages\\.upsert|cleanMessage\\(|processMessage\\(" src/Socket src/Utils
printf '\n--- group metadata normalization ---\n'
rg -n "extractGroupMetadata|groupMetadata\\(|participants.*id|jidNormalizedUser\\(.*participants" src/Socket src/Utils src/WABinary
printf '\n--- relevant slices ---\n'
sed -n '1480,1525p' src/Socket/messages-recv.ts | cat -n
sed -n '1,120p' src/Socket/groups.ts | cat -nRepository: WhiskeySockets/Baileys
Length of output: 8934
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n--- StatsManager tests ---\n'
rg -n "StatsManager|observeMessage|getTopUsers|getTopStickers|getGhosts" src/__tests__ src
printf '\n--- extractGroupMetadata implementation ---\n'
sed -n '304,420p' src/Socket/groups.ts | cat -n
printf '\n--- cleanMessage call site and message emission ---\n'
sed -n '1758,1785p' src/Socket/messages-recv.ts | cat -n
sed -n '480,520p' src/Utils/process-message.ts | cat -nRepository: WhiskeySockets/Baileys
Length of output: 8643
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n--- upsertMessage definition ---\n'
rg -n "const upsertMessage|function upsertMessage|upsertMessage =" src/Socket src/Utils
printf '\n--- messages.upsert emitters with final message shape ---\n'
sed -n '1180,1235p' src/Socket/chats.ts | cat -n
sed -n '1590,1645p' src/Socket/messages-recv.ts | cat -nRepository: WhiskeySockets/Baileys
Length of output: 4951
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n--- jid-utils slice ---\n'
sed -n '1,160p' src/WABinary/jid-utils.ts | cat -n
printf '\n--- StatsManager slice ---\n'
sed -n '61,106p' src/Framework/StatsManager.ts | cat -nRepository: WhiskeySockets/Baileys
Length of output: 7139
Normalize participant JIDs before storing or matching stats. observeMessage writes the raw participant JID, and getGhosts compares it with raw groupMetadata participant IDs using exact Map keys. PN/LID/device variants can split one user across buckets and flag active members as ghosts. Use a canonical JID/user key on both paths instead of raw string equality.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Framework/StatsManager.ts` around lines 61 - 66, Normalize participant
JIDs to a canonical user key in observeMessage before insertStmt.run, and apply
the same normalization to participant IDs used by getGhosts when building or
querying its Map. Replace raw-string matching with the shared canonicalization
utility so PN/LID/device variants resolve to the same stats entry and active
members are not classified as ghosts.
Source: Coding guidelines
| get<T>(key: string): T | undefined { | ||
| const row = this.getStmt.get(key) as { value: string } | undefined | ||
| if (row) { | ||
| try { | ||
| return JSON.parse(row.value) as T | ||
| } catch { | ||
| return row.value as unknown as T | ||
| } | ||
| } | ||
| return undefined | ||
| } | ||
|
|
||
| set<T>(key: string, value: T): void { | ||
| const strValue = typeof value === 'string' ? value : JSON.stringify(value) | ||
| this.setStmt.run(key, strValue) | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Fix data corruption caused by string serialization logic.
When typeof value === 'string', the value is stored as a raw string. Upon retrieval, JSON.parse will attempt to parse it. If the saved string happens to be a number (e.g., "123"), a boolean (e.g., "true"), or valid JSON (e.g., "{\"a\":1}"), it will be incorrectly parsed into those types instead of remaining a string. This breaks the key-value store's data integrity guarantees.
Unconditionally stringify the value on set to ensure all types (including strings) safely round-trip through JSON encoding.
🐛 Proposed fix
get<T>(key: string): T | undefined {
const row = this.getStmt.get(key) as { value: string } | undefined
if (row) {
try {
return JSON.parse(row.value) as T
} catch {
return row.value as unknown as T
}
}
return undefined
}
set<T>(key: string, value: T): void {
+ if (value === undefined) {
+ this.del(key)
+ return
+ }
- const strValue = typeof value === 'string' ? value : JSON.stringify(value)
- this.setStmt.run(key, strValue)
+ this.setStmt.run(key, JSON.stringify(value))
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| get<T>(key: string): T | undefined { | |
| const row = this.getStmt.get(key) as { value: string } | undefined | |
| if (row) { | |
| try { | |
| return JSON.parse(row.value) as T | |
| } catch { | |
| return row.value as unknown as T | |
| } | |
| } | |
| return undefined | |
| } | |
| set<T>(key: string, value: T): void { | |
| const strValue = typeof value === 'string' ? value : JSON.stringify(value) | |
| this.setStmt.run(key, strValue) | |
| } | |
| get<T>(key: string): T | undefined { | |
| const row = this.getStmt.get(key) as { value: string } | undefined | |
| if (row) { | |
| try { | |
| return JSON.parse(row.value) as T | |
| } catch { | |
| return row.value as unknown as T | |
| } | |
| } | |
| return undefined | |
| } | |
| set<T>(key: string, value: T): void { | |
| if (value === undefined) { | |
| this.del(key) | |
| return | |
| } | |
| this.setStmt.run(key, JSON.stringify(value)) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Framework/Store/SQLiteStore.ts` around lines 28 - 43, Update
SQLiteStore.set so it always JSON.stringify values, including strings, before
passing them to setStmt.run. Remove the typeof value === 'string' special case,
preserving get’s JSON.parse-based round-trip behavior and string types.
There was a problem hiding this comment.
22 issues found across 13 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/Framework/Store/SQLiteStore.ts">
<violation number="1" location="src/Framework/Store/SQLiteStore.ts:41">
P2: String cache values that are valid JSON change type on retrieval because `get` parses every stored value while `set` leaves strings unencoded. Serialize strings too so `get<string>` round-trips its declared type.</violation>
</file>
<file name="src/Framework/MediaManager.ts">
<violation number="1" location="src/Framework/MediaManager.ts:9">
P1: Loading the framework fails under this package's ESM runtime because `require` is undefined. Use an ESM import for `node-webpmux` (or `createRequire`) so importing `Context` can initialize.</violation>
<violation number="2" location="src/Framework/MediaManager.ts:25">
P3: The temp file management pattern (write input → process → read output → cleanup) is duplicated identically across both conversion methods. Extracting it into a shared helper (e.g., `private static async processWithFfmpeg(input, processor)`) would reduce duplication and prevent cleanup bugs in future conversion methods.</violation>
<violation number="3" location="src/Framework/MediaManager.ts:29">
P2: Synchronous filesystem operations (`fs.writeFileSync`, `fs.readFileSync`, `fs.copyFileSync`, `fs.unlinkSync`) block the event loop in these `async` methods. Under concurrent message load, this can stall WebSocket ping/pong and cause disconnects. Since the methods are already `async`, replace with `fs.promises.*` equivalents (`writeFile`, `readFile`, `copyFile`, `unlink`).</violation>
<violation number="4" location="src/Framework/MediaManager.ts:36">
P1: The `convertToVoiceNote` method is missing critical audio encoding parameters that WhatsApp expects for voice notes. Without specifying mono channel (`-ac 1`), sample rate, bitrate, and `-application voip`, the output OGG Opus file may be rejected by WhatsApp or play incorrectly on the receiving end. Add these options to ensure the output is a standard mono Opus voice note at an appropriate bitrate.</violation>
<violation number="5" location="src/Framework/MediaManager.ts:66">
P2: Sticker pack metadata is encoded with a zero EXIF payload length, so WhatsApp can ignore `packname` and `author`. Write the serialized JSON length into offset 14 before saving the image.</violation>
</file>
<file name="README.md">
<violation number="1" location="README.md:50">
P1: The `creds.update` listener in the example will never fire because `bot.socket` is `undefined` before `bot.start()` is called. The optional chaining `?.` makes this a silent no-op — credentials are never persisted, which means the authentication state is lost on restart and the user will be forced to re-authenticate. Move `bot.socket.ev.on('creds.update', saveCreds)` to **after** `await bot.start()` to ensure the socket exists. Note that reconnection will create a new socket without the listener, so an internal hook in `Bot.start()` or a wrapper around the socket creation would be more robust.</violation>
</file>
<file name="src/Framework/StatsManager.ts">
<violation number="1" location="src/Framework/StatsManager.ts:66">
P1: JIDs stored/queried without normalization — device suffixes can cause ghost-detection false positives.
`observeMessage` stores the raw `userJid` parameter into the `group_stats` table. But `getGhosts` compares stored JIDs against `groupMetadata().participants.map(p => p.id)`, which returns device-stripped JIDs. When a `userJid` carries a device suffix (`:0`, `:42`), the `Map.get(jid)` lookup will miss the participant, classifying them as a never-spoken ghost even though they have sent messages.
**Recommendation:** Normalize all JIDs with `jidNormalizedUser` before storing and querying.
```ts
// In observeMessage:
const normalizedUserJid = jidNormalizedUser(userJid)
this.insertStmt.run(jidNormalizedUser(groupJid), normalizedUserJid, msgCount, stickerCount, now)
// In getGhosts:
const normalizedJid = jidNormalizedUser(jid)
const lastActive = statsMap.get(normalizedJid)
```</violation>
<violation number="2" location="src/Framework/StatsManager.ts:69">
P2: Negative `limit` returns every member instead of a bounded leaderboard, since SQLite treats negative `LIMIT` as unlimited. Validate a non-negative integer before executing either leaderboard query.</violation>
<violation number="3" location="src/Framework/StatsManager.ts:78">
P2: Calling `getGhosts` while reconnecting reaches `groupMetadata` on a closed socket rather than failing as "not connected." Include `isConnected` in the availability guard.</violation>
<violation number="4" location="src/Framework/StatsManager.ts:79">
P2: Plain `Error` for socket-disconnected state — downstream code can't distinguish this from internal assertion failures.
AGENTS.md specifies that protocol/connection errors should use `Boom` with a `statusCode` so callers can branch on `error.output.statusCode`. Currently, `getGhosts` throws a bare `Error`, and any code that tries to retry on connection errors has no programmatic way to identify the failure type.
**Recommendation:** Throw `new Boom('Socket not connected', { statusCode: 503 })` instead, consistent with the project convention.</violation>
</file>
<file name="src/Framework/Bot.ts">
<violation number="1" location="src/Framework/Bot.ts:46">
P2: Multiple `Bot` instances in one working directory silently share sessions, analytics, and retry counters through `baileys_store.db`; one bot can overwrite another's state. Make the store/path configurable or derive an isolated path/namespace per bot.</violation>
<violation number="2" location="src/Framework/Bot.ts:65">
P2: `startsWith(cmd)` matches partial overlaps — e.g., registering `!sticker` would also fire for `!stickerSpam` or `!stickerset`. Require an exact match or a word boundary (whitespace) after the command prefix to avoid unintended handler invocations.</violation>
<violation number="3" location="src/Framework/Bot.ts:87">
P2: Queued `sendMessage` calls remain pending forever after `DisconnectReason.loggedOut`, so callers awaiting a reply/send can hang indefinitely. Reject and clear queued entries on terminal close (and define a bounded/discard policy for prolonged disconnection).</violation>
<violation number="4" location="src/Framework/Bot.ts:88">
P2: Disconnected sends expose recipient JIDs in unstructured stdout logs and bypass the configured logger. Use structured logging and avoid logging the JID unless required for an approved diagnostic path.</violation>
<violation number="5" location="src/Framework/Bot.ts:88">
P2: Bot.ts uses `console.log` for all logging output, but the codebase convention requires pino-compatible structured logging via a `logger: ILogger` instance. Every async-boundary path should accept and use a logger. Without it, log processors can't index or filter these messages, and downstream users can't control log levels. Accept a `logger` in BotConfig and use `this.logger.info(...)` / `this.logger.warn(...)` with structured payloads (e.g. `{ queueLength: this.messageQueue.length }`) instead of console.log string interpolation.</violation>
<violation number="6" location="src/Framework/Bot.ts:120">
P1: `this.stats.observeMessage(...)` executes synchronous SQLite I/O outside the `try` block. If it throws (e.g., database locked, disk full), the entire async event handler rejects and all middleware for that message is skipped. Move this call inside the `try` block so a stats failure doesn't prevent message processing.</violation>
<violation number="7" location="src/Framework/Bot.ts:156">
P2: Reconnect scheduling fails lint and can leave a failed `start()` rejection unhandled because its Promise is dropped. Explicitly handle the reconnect promise.</violation>
</file>
<file name="src/Framework/Context.ts">
<violation number="1" location="src/Framework/Context.ts:3">
P1: `makeWASocket` is only used in a type context (`typeof makeWASocket` inside a `ReturnType<>`) — never called or used as a runtime value. With `verbatimModuleSyntax`, this must be `import type` to avoid emitting an unnecessary runtime import.</violation>
<violation number="2" location="src/Framework/Context.ts:8">
P3: The new public Context helpers have no unit coverage for reply quoting, session delegation, reactions, or media conversion failures. Add colocated Context tests so this API contract is regression-tested.</violation>
<violation number="3" location="src/Framework/Context.ts:19">
P2: The `text` getter only extracts text from `conversation` and `extendedTextMessage.text`, missing captions on image, video, document, and other media messages. When a user sends an image with a caption like "Check this out", `ctx.text` returns `undefined` and text-based handlers silently skip the message. Add media caption fields to the getter, for example `message?.imageMessage?.caption`, `message?.videoMessage?.caption`, `message?.documentMessage?.caption`.</violation>
</file>
<file name="Example/bot-example.ts">
<violation number="1" location="Example/bot-example.ts:51">
P0: The `saveCreds` listener is registered on `bot.socket?.ev` before `bot.start()` is called, but `bot.socket` is only assigned during `start()`. Because of the `?.` optional chaining, this line silently becomes a no-op, and `saveCreds` is never actually registered — the bot will lose its session on restart because `creds.update` events are never persisted. Move the listener registration after `await bot.start()`, or have the `Bot` class handle creds saving internally.</violation>
</file>
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
Re-trigger cubic
| }) | ||
|
|
||
| // Guardar credenciales al conectarse/actualizarse | ||
| bot.socket?.ev.on('creds.update', saveCreds) |
There was a problem hiding this comment.
P0: The saveCreds listener is registered on bot.socket?.ev before bot.start() is called, but bot.socket is only assigned during start(). Because of the ?. optional chaining, this line silently becomes a no-op, and saveCreds is never actually registered — the bot will lose its session on restart because creds.update events are never persisted. Move the listener registration after await bot.start(), or have the Bot class handle creds saving internally.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Example/bot-example.ts, line 51:
<comment>The `saveCreds` listener is registered on `bot.socket?.ev` before `bot.start()` is called, but `bot.socket` is only assigned during `start()`. Because of the `?.` optional chaining, this line silently becomes a no-op, and `saveCreds` is never actually registered — the bot will lose its session on restart because `creds.update` events are never persisted. Move the listener registration after `await bot.start()`, or have the `Bot` class handle creds saving internally.</comment>
<file context>
@@ -0,0 +1,57 @@
+ })
+
+ // Guardar credenciales al conectarse/actualizarse
+ bot.socket?.ev.on('creds.update', saveCreds)
+
+ await bot.start()
</file context>
| import ffmpegStatic from 'ffmpeg-static' | ||
|
|
||
| // node-webpmux usa commonjs en su mayoría | ||
| const webpmux = require('node-webpmux') |
There was a problem hiding this comment.
P1: Loading the framework fails under this package's ESM runtime because require is undefined. Use an ESM import for node-webpmux (or createRequire) so importing Context can initialize.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/MediaManager.ts, line 9:
<comment>Loading the framework fails under this package's ESM runtime because `require` is undefined. Use an ESM import for `node-webpmux` (or `createRequire`) so importing `Context` can initialize.</comment>
<file context>
@@ -0,0 +1,109 @@
+import ffmpegStatic from 'ffmpeg-static'
+
+// node-webpmux usa commonjs en su mayoría
+const webpmux = require('node-webpmux')
+
+// Configurar la ruta de FFmpeg automáticamente
</file context>
| bot.socket?.ev.on('creds.update', saveCreds) | ||
| await bot.start() |
There was a problem hiding this comment.
P1: The creds.update listener in the example will never fire because bot.socket is undefined before bot.start() is called. The optional chaining ?. makes this a silent no-op — credentials are never persisted, which means the authentication state is lost on restart and the user will be forced to re-authenticate. Move bot.socket.ev.on('creds.update', saveCreds) to after await bot.start() to ensure the socket exists. Note that reconnection will create a new socket without the listener, so an internal hook in Bot.start() or a wrapper around the socket creation would be more robust.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 50:
<comment>The `creds.update` listener in the example will never fire because `bot.socket` is `undefined` before `bot.start()` is called. The optional chaining `?.` makes this a silent no-op — credentials are never persisted, which means the authentication state is lost on restart and the user will be forced to re-authenticate. Move `bot.socket.ev.on('creds.update', saveCreds)` to **after** `await bot.start()` to ensure the socket exists. Note that reconnection will create a new socket without the listener, so an internal hook in `Bot.start()` or a wrapper around the socket creation would be more robust.</comment>
<file context>
@@ -10,6 +10,51 @@
+ await ctx.replySticker(buffer, { packname: 'SuperBot', author: '@luis' })
+ })
+
+ bot.socket?.ev.on('creds.update', saveCreds)
+ await bot.start()
+}
</file context>
| bot.socket?.ev.on('creds.update', saveCreds) | |
| await bot.start() | |
| await bot.start() | |
| bot.socket?.ev.on('creds.update', saveCreds) |
|
|
||
| try { | ||
| await new Promise<void>((resolve, reject) => { | ||
| ffmpeg(tempInput) |
There was a problem hiding this comment.
P1: The convertToVoiceNote method is missing critical audio encoding parameters that WhatsApp expects for voice notes. Without specifying mono channel (-ac 1), sample rate, bitrate, and -application voip, the output OGG Opus file may be rejected by WhatsApp or play incorrectly on the receiving end. Add these options to ensure the output is a standard mono Opus voice note at an appropriate bitrate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/MediaManager.ts, line 36:
<comment>The `convertToVoiceNote` method is missing critical audio encoding parameters that WhatsApp expects for voice notes. Without specifying mono channel (`-ac 1`), sample rate, bitrate, and `-application voip`, the output OGG Opus file may be rejected by WhatsApp or play incorrectly on the receiving end. Add these options to ensure the output is a standard mono Opus voice note at an appropriate bitrate.</comment>
<file context>
@@ -0,0 +1,109 @@
+
+ try {
+ await new Promise<void>((resolve, reject) => {
+ ffmpeg(tempInput)
+ .inputOptions(['-y'])
+ .outputOptions([
</file context>
| @@ -0,0 +1,67 @@ | |||
| import type { WAMessage, AnyMessageContent, MiscMessageGenerationOptions } from '../Types' | |||
| import type { Bot } from './Bot' | |||
| import makeWASocket from '../Socket' | |||
There was a problem hiding this comment.
P1: makeWASocket is only used in a type context (typeof makeWASocket inside a ReturnType<>) — never called or used as a runtime value. With verbatimModuleSyntax, this must be import type to avoid emitting an unnecessary runtime import.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/Context.ts, line 3:
<comment>`makeWASocket` is only used in a type context (`typeof makeWASocket` inside a `ReturnType<>`) — never called or used as a runtime value. With `verbatimModuleSyntax`, this must be `import type` to avoid emitting an unnecessary runtime import.</comment>
<file context>
@@ -0,0 +1,67 @@
+import type { WAMessage, AnyMessageContent, MiscMessageGenerationOptions } from '../Types'
+import type { Bot } from './Bot'
+import makeWASocket from '../Socket'
+import { MediaManager, type StickerMetadata } from './MediaManager'
+
</file context>
|
|
||
| public async getGhosts(groupJid: string, inactiveDays: number = 30): Promise<{ jid: string, isTotalGhost: boolean, lastActive?: number }[]> { | ||
| if (!this.bot.socket) { | ||
| throw new Error('Socket not connected') |
There was a problem hiding this comment.
P2: Plain Error for socket-disconnected state — downstream code can't distinguish this from internal assertion failures.
AGENTS.md specifies that protocol/connection errors should use Boom with a statusCode so callers can branch on error.output.statusCode. Currently, getGhosts throws a bare Error, and any code that tries to retry on connection errors has no programmatic way to identify the failure type.
Recommendation: Throw new Boom('Socket not connected', { statusCode: 503 }) instead, consistent with the project convention.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/StatsManager.ts, line 79:
<comment>Plain `Error` for socket-disconnected state — downstream code can't distinguish this from internal assertion failures.
AGENTS.md specifies that protocol/connection errors should use `Boom` with a `statusCode` so callers can branch on `error.output.statusCode`. Currently, `getGhosts` throws a bare `Error`, and any code that tries to retry on connection errors has no programmatic way to identify the failure type.
**Recommendation:** Throw `new Boom('Socket not connected', { statusCode: 503 })` instead, consistent with the project convention.</comment>
<file context>
@@ -0,0 +1,108 @@
+
+ public async getGhosts(groupJid: string, inactiveDays: number = 30): Promise<{ jid: string, isTotalGhost: boolean, lastActive?: number }[]> {
+ if (!this.bot.socket) {
+ throw new Error('Socket not connected')
+ }
+
</file context>
|
|
||
| public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) { | ||
| this.use(async (ctx, next) => { | ||
| if (ctx.text?.startsWith(cmd)) { |
There was a problem hiding this comment.
P2: startsWith(cmd) matches partial overlaps — e.g., registering !sticker would also fire for !stickerSpam or !stickerset. Require an exact match or a word boundary (whitespace) after the command prefix to avoid unintended handler invocations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/Bot.ts, line 65:
<comment>`startsWith(cmd)` matches partial overlaps — e.g., registering `!sticker` would also fire for `!stickerSpam` or `!stickerset`. Require an exact match or a word boundary (whitespace) after the command prefix to avoid unintended handler invocations.</comment>
<file context>
@@ -0,0 +1,177 @@
+
+ public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) {
+ this.use(async (ctx, next) => {
+ if (ctx.text?.startsWith(cmd)) {
+ await handler(ctx)
+ }
</file context>
| @@ -0,0 +1,109 @@ | |||
| import * as fs from 'fs' | |||
There was a problem hiding this comment.
P2: Synchronous filesystem operations (fs.writeFileSync, fs.readFileSync, fs.copyFileSync, fs.unlinkSync) block the event loop in these async methods. Under concurrent message load, this can stall WebSocket ping/pong and cause disconnects. Since the methods are already async, replace with fs.promises.* equivalents (writeFile, readFile, copyFile, unlink).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/MediaManager.ts, line 29:
<comment>Synchronous filesystem operations (`fs.writeFileSync`, `fs.readFileSync`, `fs.copyFileSync`, `fs.unlinkSync`) block the event loop in these `async` methods. Under concurrent message load, this can stall WebSocket ping/pong and cause disconnects. Since the methods are already `async`, replace with `fs.promises.*` equivalents (`writeFile`, `readFile`, `copyFile`, `unlink`).</comment>
<file context>
@@ -0,0 +1,109 @@
+ const tempOutput = this.getTempFile('webp')
+
+ if (Buffer.isBuffer(inputPathOrBuffer)) {
+ fs.writeFileSync(tempInput, inputPathOrBuffer)
+ } else {
+ fs.copyFileSync(inputPathOrBuffer, tempInput)
</file context>
| @@ -0,0 +1,67 @@ | |||
| import type { WAMessage, AnyMessageContent, MiscMessageGenerationOptions } from '../Types' | |||
There was a problem hiding this comment.
P3: The new public Context helpers have no unit coverage for reply quoting, session delegation, reactions, or media conversion failures. Add colocated Context tests so this API contract is regression-tested.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/Context.ts, line 8:
<comment>The new public Context helpers have no unit coverage for reply quoting, session delegation, reactions, or media conversion failures. Add colocated Context tests so this API contract is regression-tested.</comment>
<file context>
@@ -0,0 +1,67 @@
+
+export type WASocket = ReturnType<typeof makeWASocket>
+
+export class Context {
+ public readonly message: WAMessage
+ public readonly bot: Bot
</file context>
| } | ||
|
|
||
| public static async convertToSticker(inputPathOrBuffer: string | Buffer, metadata?: StickerMetadata): Promise<Buffer> { | ||
| const tempInput = this.getTempFile('in') |
There was a problem hiding this comment.
P3: The temp file management pattern (write input → process → read output → cleanup) is duplicated identically across both conversion methods. Extracting it into a shared helper (e.g., private static async processWithFfmpeg(input, processor)) would reduce duplication and prevent cleanup bugs in future conversion methods.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/MediaManager.ts, line 25:
<comment>The temp file management pattern (write input → process → read output → cleanup) is duplicated identically across both conversion methods. Extracting it into a shared helper (e.g., `private static async processWithFfmpeg(input, processor)`) would reduce duplication and prevent cleanup bugs in future conversion methods.</comment>
<file context>
@@ -0,0 +1,109 @@
+ }
+
+ public static async convertToSticker(inputPathOrBuffer: string | Buffer, metadata?: StickerMetadata): Promise<Buffer> {
+ const tempInput = this.getTempFile('in')
+ const tempOutput = this.getTempFile('webp')
+
</file context>
|
This PR is stale because it has been open for 14 days with no activity. Remove the stale label or comment or this will be closed in 14 days |
Summary by cubic
Adds an enterprise-ready bot framework with a high-level
BotAPI, durable SQLite-backed state, media conversion tools, and group analytics to build resilient production bots.New Features
Bot: middleware andbot.command(), message queue with exponential backoff reconnect.Context:reply,react,replySticker,replyVoiceNote, and simple per-chat session API.SQLiteStore: KV store viabetter-sqlite3used formsgRetryCounterCacheto reduce RAM use.MediaManager: image/video to WebP stickers with metadata; audio to OGG voice notes.StatsManager: group activity tracking andgetGhosts(groupId)for inactive members.Example/bot-example.ts, README migration notes, and public exports viasrc/index.ts.Dependencies
better-sqlite3,ffmpeg-static,fluent-ffmpeg,node-webpmux.@types/better-sqlite3,@types/fluent-ffmpeg.Written for commit 4bb3bc8. Summary will update on new commits.
Summary by CodeRabbit
New Features
Documentation