From 55480be86f3ea266550e253a65b3a8642418c847 Mon Sep 17 00:00:00 2001 From: Yudhi Armyndharis Date: Sun, 5 Jul 2026 21:16:14 +0700 Subject: [PATCH] fix(channels): return channel messages instead of an always-empty [] (#625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit whatsapp-web.js 1.34.x has no client.getChannelById; the adapter called it, so getChannelMessages/getChannelById always threw and the error was swallowed (into [] / null). GET /channels worked only because it uses the real getChannels(). - Resolve the channel from getChannels() (whose items are Channel instances that carry fetchMessages()) and read its messages; stop swallowing errors. - An unknown/unsubscribed channel now returns 404 (ChannelNotFoundError, a NotFoundException subclass) instead of a silent empty 200 — consistent with getChannelById/findOne. - Drop the phantom getChannelById from the BusinessClient type. --- CHANGELOG.md | 4 ++ src/common/errors/channel-not-found.error.ts | 16 +++++ .../adapters/whatsapp-web-js.adapter.spec.ts | 56 ++++++++++++++++++ .../adapters/whatsapp-web-js.adapter.ts | 59 +++++++------------ src/engine/types/whatsapp-web-js.types.ts | 3 +- 5 files changed, 99 insertions(+), 39 deletions(-) create mode 100644 src/common/errors/channel-not-found.error.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d1124f3e0..f1041fd2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Native WhatsApp polls** via `POST /api/sessions/:sessionId/messages/send-poll`: question, 2–12 options and an optional `allowMultipleAnswers` flag (default single choice), implemented on both engines (whatsapp-web.js `Poll`, Baileys `poll` content with `selectableCount` 1/0). The message history stores the poll question as the body so the log stays readable. Polls are a first-class `poll` message type end to end — both engines map incoming poll messages to it, so the websocket/webhook events, persisted rows, and dashboard all report `poll` consistently. Thanks @alejo117. +### Fixed + +- **`GET /api/sessions/:sessionId/channels/:channelId/messages` always returned an empty array** on the whatsapp-web.js engine (#625). The adapter called `client.getChannelById()`, which does not exist in whatsapp-web.js 1.34.x, so every call threw and the error was swallowed into `[]`. Channel messages are now read from the subscribed `Channel` instance (via `getChannels()`), and an unknown/unsubscribed channel returns a `404` (`ChannelNotFoundError`) instead of a silent empty `200` — matching `GET /channels/:channelId`. Thanks @Header9968. + ## [0.8.7] - 2026-07-03 ### Added diff --git a/src/common/errors/channel-not-found.error.ts b/src/common/errors/channel-not-found.error.ts new file mode 100644 index 000000000..0e94f02f6 --- /dev/null +++ b/src/common/errors/channel-not-found.error.ts @@ -0,0 +1,16 @@ +import { NotFoundException } from '@nestjs/common'; + +/** + * Thrown by the engine layer when a channel referenced by id isn't among the session's subscribed + * channels (a wrong/typo'd id, or a channel not yet synced into the local collection). + * + * Extends NestJS `NotFoundException` so it maps to **HTTP 404** through the built-in exception + * handler — i.e. it does NOT depend on a custom global filter being registered, and it survives the + * `channel.service` passthrough — instead of surfacing as a generic 500 Internal Server Error. + * Mirrors {@link MessageNotFoundError}. + */ +export class ChannelNotFoundError extends NotFoundException { + constructor(channelId: string) { + super(`Channel ${channelId} not found`); + } +} diff --git a/src/engine/adapters/whatsapp-web-js.adapter.spec.ts b/src/engine/adapters/whatsapp-web-js.adapter.spec.ts index 50c59d725..925a40a5a 100644 --- a/src/engine/adapters/whatsapp-web-js.adapter.spec.ts +++ b/src/engine/adapters/whatsapp-web-js.adapter.spec.ts @@ -16,6 +16,7 @@ import * as qrcode from 'qrcode'; import { UnprocessableEntityException } from '@nestjs/common'; import { EngineNotReadyError } from '../../common/errors/engine-not-ready.error'; import { EngineNotSupportedError } from '../../common/errors/engine-not-supported.error'; +import { ChannelNotFoundError } from '../../common/errors/channel-not-found.error'; import { EngineStatus } from '../interfaces/whatsapp-engine.interface'; import { SsrfBlockedError } from '../../common/security/ssrf-guard'; import { fetch as undiciFetch } from 'undici'; @@ -365,6 +366,61 @@ describe('WhatsAppWebJsAdapter.forwardMessage (returns the real sent id, not a s }); }); +describe('WhatsAppWebJsAdapter channels (#625 — wwebjs Client has no getChannelById)', () => { + const CHANNEL = '120363401234567890@newsletter'; + + const readyAdapter = (client: unknown): WhatsAppWebJsAdapter => { + const adapter = new WhatsAppWebJsAdapter({ sessionId: 's', sessionDataPath: './data/sessions', puppeteer: {} }); + (adapter as unknown as { status: EngineStatus }).status = EngineStatus.READY; + (adapter as unknown as { client: unknown }).client = client; + return adapter; + }; + + it('getChannelMessages fetches via the subscribed Channel (getChannels), not the non-existent getChannelById', async () => { + const fetchMessages = jest + .fn() + .mockResolvedValue([{ id: { _serialized: 'M1' }, body: 'hello', timestamp: 1700000000, hasMedia: false }]); + const getChannels = jest.fn().mockResolvedValue([{ id: { _serialized: CHANNEL }, name: 'News', fetchMessages }]); + + const result = await readyAdapter({ getChannels }).getChannelMessages(CHANNEL, 10); + + expect(getChannels).toHaveBeenCalled(); + expect(fetchMessages).toHaveBeenCalledWith({ limit: 10 }); + expect(result).toEqual([{ id: 'M1', body: 'hello', timestamp: 1700000000, hasMedia: false, mediaUrl: undefined }]); + }); + + it('getChannelMessages surfaces a not-found channel as ChannelNotFoundError (→ 404), not a silent []', async () => { + const getChannels = jest + .fn() + .mockResolvedValue([{ id: { _serialized: 'other@newsletter' }, fetchMessages: jest.fn() }]); + // Typed NotFoundException subclass so it maps to 404, not a plain Error → generic 500. + await expect(readyAdapter({ getChannels }).getChannelMessages(CHANNEL)).rejects.toBeInstanceOf( + ChannelNotFoundError, + ); + }); + + it('getChannelMessages returns [] for a channel with no messages (empty is not an error)', async () => { + const fetchMessages = jest.fn().mockResolvedValue([]); + const getChannels = jest.fn().mockResolvedValue([{ id: { _serialized: CHANNEL }, name: 'News', fetchMessages }]); + await expect(readyAdapter({ getChannels }).getChannelMessages(CHANNEL)).resolves.toEqual([]); + }); + + it('getChannelById resolves from the subscribed-channel list (no getChannelById call)', async () => { + const getChannels = jest + .fn() + .mockResolvedValue([ + { id: { _serialized: CHANNEL }, name: 'News', description: 'desc', subscriberCount: 5, verified: true }, + ]); + const ch = await readyAdapter({ getChannels }).getChannelById(CHANNEL); + expect(ch).toMatchObject({ id: CHANNEL, name: 'News', description: 'desc', subscriberCount: 5, verified: true }); + }); + + it('getChannelById returns null for a channel not in the subscribed list (service maps null → 404)', async () => { + const getChannels = jest.fn().mockResolvedValue([{ id: { _serialized: 'other@newsletter' }, name: 'Other' }]); + await expect(readyAdapter({ getChannels }).getChannelById(CHANNEL)).resolves.toBeNull(); + }); +}); + describe('WhatsAppWebJsAdapter channel-JID guard (#554 — wwebjs Channel lacks Chat methods)', () => { const NEWSLETTER = '120363401234567890@newsletter'; const USER = '628111@c.us'; diff --git a/src/engine/adapters/whatsapp-web-js.adapter.ts b/src/engine/adapters/whatsapp-web-js.adapter.ts index cf219d295..7cdc38a91 100644 --- a/src/engine/adapters/whatsapp-web-js.adapter.ts +++ b/src/engine/adapters/whatsapp-web-js.adapter.ts @@ -42,6 +42,7 @@ import { createLogger } from '../../common/services/logger.service'; import { EngineNotReadyError } from '../../common/errors/engine-not-ready.error'; import { EngineNotSupportedError } from '../../common/errors/engine-not-supported.error'; import { MessageNotFoundError } from '../../common/errors/message-not-found.error'; +import { ChannelNotFoundError } from '../../common/errors/channel-not-found.error'; import { loadRemoteMediaBuffer } from '../../common/media/load-remote-media'; import { GroupChat, @@ -1463,23 +1464,9 @@ export class WhatsAppWebJsAdapter extends EventEmitter implements IWhatsAppEngin async getChannelById(channelId: string): Promise { this.ensureReady(); - try { - const ch = await (this.client as unknown as BusinessClient).getChannelById(channelId); - if (!ch) { - return null; - } - return { - id: String(typeof ch.id === 'object' ? ch.id._serialized : ch.id), - name: String(ch.name || ''), - description: ch.description ? String(ch.description) : undefined, - inviteCode: ch.inviteCode ? String(ch.inviteCode) : undefined, - subscriberCount: ch.subscriberCount ? Number(ch.subscriberCount) : undefined, - verified: ch.verified ? Boolean(ch.verified) : undefined, - }; - } catch (error) { - this.logger.warn(`Failed to get channel: ${channelId}`, String(error)); - return null; - } + // wwebjs 1.34.x exposes no client.getChannelById; resolve from the subscribed-channel list (#625). + const channels = await this.getSubscribedChannels(); + return channels.find(c => c.id === channelId) ?? null; } async subscribeToChannel(inviteCode: string): Promise { @@ -1501,26 +1488,24 @@ export class WhatsAppWebJsAdapter extends EventEmitter implements IWhatsAppEngin async getChannelMessages(channelId: string, limit: number = 50): Promise { this.ensureReady(); - try { - const ch = await (this.client as unknown as BusinessClient).getChannelById(channelId); - if (!ch) { - throw new Error(`Channel ${channelId} not found`); - } - const messages = await ch.fetchMessages({ limit }); - if (!messages) { - return []; - } - return messages.map(msg => ({ - id: String(typeof msg.id === 'object' ? msg.id._serialized : msg.id), - body: String(msg.body || ''), - timestamp: Number(msg.timestamp), - hasMedia: Boolean(msg.hasMedia), - mediaUrl: msg.mediaUrl ? String(msg.mediaUrl) : undefined, - })); - } catch (error) { - this.logger.error(`Failed to get channel messages: ${String(error)}`); - return []; - } + // wwebjs 1.34.x has no client.getChannelById (calling it threw and the error was swallowed into an + // empty list, #625). The subscribed Channel instances returned by getChannels() carry fetchMessages(), + // so resolve the channel from that list and read its messages. A missing channel surfaces as a + // ChannelNotFoundError (→ 404, like getChannelById) so callers can tell "no messages" apart from + // "wrong/unsubscribed channel" instead of getting a silent []. + const channels = await (this.client as unknown as BusinessClient).getChannels(); + const channel = channels?.find(c => (typeof c.id === 'object' ? c.id._serialized : c.id) === channelId); + if (!channel) { + throw new ChannelNotFoundError(channelId); + } + const messages = await channel.fetchMessages({ limit }); + return (messages ?? []).map(msg => ({ + id: String(typeof msg.id === 'object' ? msg.id._serialized : msg.id), + body: String(msg.body || ''), + timestamp: Number(msg.timestamp), + hasMedia: Boolean(msg.hasMedia), + mediaUrl: msg.mediaUrl ? String(msg.mediaUrl) : undefined, + })); } // ========== Gap Quick Wins Implementation ========== diff --git a/src/engine/types/whatsapp-web-js.types.ts b/src/engine/types/whatsapp-web-js.types.ts index 0b171fa7a..e505722e5 100644 --- a/src/engine/types/whatsapp-web-js.types.ts +++ b/src/engine/types/whatsapp-web-js.types.ts @@ -72,12 +72,11 @@ export interface MessageWithReactions extends Omit { getLabels(): Promise>; getLabelById(id: string): Promise<{ id: string; name: string; hexColor: string } | null>; getChannels(): Promise; - getChannelById(id: string): Promise; subscribeToChannel(inviteCode: string): Promise; unsubscribeFromChannel(id: string): Promise; }