Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Opt-in deep chat history (`deep=true`).** `GET /sessions/:id/messages/:chatId/history` was capped at
100 messages per request — OpenWA's own bound, not a WhatsApp limit, since whatsapp-web.js can load
earlier messages on demand. A new `deep=true` query raises the ceiling to 2000 so callers can reach
weeks/months back. Deep mode is metadata-only (it ignores `includeMedia`, since base64 for up to 2000
messages would be an enormous payload). The default path is unchanged (default 50, max 100). The Baileys
engine has no history sync, so the endpoint still returns `501` there regardless of `deep`. (#347)

### Fixed

- **Baileys engine: inbound message ids are now engine-neutral (`@c.us`).** The Baileys adapter emitted
Expand Down
27 changes: 21 additions & 6 deletions docs/examples/chat-history-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,32 @@ This endpoint asks the active WhatsApp engine for recent messages in a chat. It
The endpoint is intentionally bounded:

- `limit` defaults to `50`.
- `limit` is clamped to the range `1`–`100`.
- `limit` is clamped to the range `1`–`100` (or `1`–`2000` with `deep=true`, see below).
- Values such as `limit=999` do not request unbounded history; they are reduced to the maximum allowed limit.
- `includeMedia=true` downloads media data and is slower than metadata-only history.
- `deep=true` raises the ceiling to `2000` for reaching further back, and forces metadata-only.

## How Deep It Can Reach

The live history endpoint returns at most the **100 most recent** messages per request (the `limit` clamp
above). The `whatsapp-web.js` engine *can* load older messages on demand — internally it drives WhatsApp
Web's "load earlier messages" mechanism — so reaching further back is bounded by **OpenWA's current cap**,
not by what WhatsApp Web is willing to expose. To go back weeks or months you would need a much larger
window than 100; a deeper-history mode is tracked in [#347](https://github.com/rmyndharis/OpenWA/issues/347).
By default the live history endpoint returns at most the **100 most recent** messages per request (the
`limit` clamp above). The `whatsapp-web.js` engine *can* load older messages on demand — internally it
drives WhatsApp Web's "load earlier messages" mechanism — so reaching further back is bounded by
**OpenWA's cap**, not by what WhatsApp Web is willing to expose.

To go back weeks or months, set `deep=true`. This raises the ceiling to **2000** messages per request:

```http
GET /api/sessions/{sessionId}/messages/{chatId}/history?limit=2000&deep=true
```

Deep mode is **metadata-only** — `includeMedia` is ignored, because downloading base64 media for up to
2000 messages would produce an enormous, slow response. Fetch media separately for the specific messages
you need. Note that a very large, rapid history pull is heavier on the linked session and can increase the
risk of WhatsApp rate-limiting; use the smallest window that meets your need.

Deep mode applies to the `whatsapp-web.js` engine. The Baileys engine does not expose on-demand history
(it has no message-history sync), so the history endpoint returns `501 Not Implemented` there regardless
of `deep`; consume Baileys history through local storage / webhooks / WebSocket as it arrives instead.

There is still an ultimate ceiling: once WhatsApp's servers stop returning older messages for the linked
session, no further history is retrievable through the web engine, regardless of `limit`. So the endpoint
Expand Down
13 changes: 12 additions & 1 deletion src/modules/message/message.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,21 +272,32 @@ export class MessageController {
type: Boolean,
description: 'When true, downloads media (base64) for messages that have it. Slower; default false.',
})
@ApiQuery({
name: 'deep',
required: false,
type: Boolean,
description:
'When true, raises the limit ceiling from 100 to 2000 for reaching further back in history ' +
'(whatsapp-web.js only; loads earlier messages on demand). Forces metadata-only (includeMedia ' +
'is ignored). Large/slow requests may increase WhatsApp rate-limiting risk; default false.',
})
@ApiResponse({ status: 200, description: 'Chat history (most recent messages)' })
async getChatHistory(
@Param('sessionId') sessionId: string,
@Param('chatId') chatId: string,
@Query('limit') limit?: string,
@Query('includeMedia') includeMedia?: string,
@Query('deep') deep?: string,
) {
// Parse the limit defensively: a non-numeric query value (?limit=abc) yields NaN,
// so fall back to undefined and let the service apply its default + [1,100] clamp.
// so fall back to undefined and let the service apply its default + clamp.
const parsedLimit = limit ? parseInt(limit, 10) : undefined;
return this.messageService.getChatHistory(
sessionId,
chatId,
parsedLimit !== undefined && !Number.isNaN(parsedLimit) ? parsedLimit : undefined,
includeMedia === 'true' || includeMedia === '1',
deep === 'true' || deep === '1',
);
}

Expand Down
22 changes: 22 additions & 0 deletions src/modules/message/message.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,28 @@ describe('MessageService', () => {
const result = await service.getChatHistory('sess-1', 'test@c.us');
expect(result).toBe(fake);
});

describe('deep mode (#347)', () => {
it('allows a limit above the standard 100 cap when deep=true', async () => {
await service.getChatHistory('sess-1', 'test@c.us', 500, false, true);
expect(mockEngine.getChatHistory).toHaveBeenLastCalledWith('test@c.us', 500, false);
});

it('clamps a deep limit to the 2000 ceiling', async () => {
await service.getChatHistory('sess-1', 'test@c.us', 5000, false, true);
expect(mockEngine.getChatHistory).toHaveBeenLastCalledWith('test@c.us', 2000, false);
});

it('forces includeMedia off in deep mode (metadata-only)', async () => {
await service.getChatHistory('sess-1', 'test@c.us', 300, true, true);
expect(mockEngine.getChatHistory).toHaveBeenLastCalledWith('test@c.us', 300, false);
});

it('still clamps to 100 when deep is not set (regression guard)', async () => {
await service.getChatHistory('sess-1', 'test@c.us', 500, false, false);
expect(mockEngine.getChatHistory).toHaveBeenLastCalledWith('test@c.us', 100, false);
});
});
});

describe('deleteMessage', () => {
Expand Down
18 changes: 11 additions & 7 deletions src/modules/message/message.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,20 +530,24 @@ export class MessageService {
/** Maximum messages a single getChatHistory call may request from the engine. */
private static readonly MAX_CHAT_HISTORY_LIMIT = 100;

/** Higher ceiling for opt-in deep history (`deep=true`). Bounded so a caller still can't ask unbounded. */
private static readonly MAX_DEEP_CHAT_HISTORY_LIMIT = 2000;

/**
* Fetch chat history live from WhatsApp (bypasses local DB).
* Returns the most recent `limit` messages for the given chat.
* When `includeMedia` is true, downloads media (base64) for messages that have it.
*
* `limit` is clamped to [1, 100] (and falls back to 50 for non-finite input) so a
* caller cannot ask the engine to fetch an unbounded number of messages.
* `limit` is clamped to [1, 100] (and falls back to 50 for non-finite input) so a caller cannot ask the
* engine to fetch an unbounded number of messages. When `deep` is true the ceiling is raised to 2000
* (for reaching weeks/months back on whatsapp-web.js, which can load earlier messages on demand) and
* media is forced off — downloading base64 for up to 2000 messages would be an enormous, slow payload.
*/
async getChatHistory(sessionId: string, chatId: string, limit = 50, includeMedia = false) {
async getChatHistory(sessionId: string, chatId: string, limit = 50, includeMedia = false, deep = false) {
const engine = this.getEngine(sessionId);
const safeLimit = Number.isFinite(limit)
? Math.min(Math.max(Math.trunc(limit), 1), MessageService.MAX_CHAT_HISTORY_LIMIT)
: 50;
return engine.getChatHistory(chatId, safeLimit, includeMedia);
const ceiling = deep ? MessageService.MAX_DEEP_CHAT_HISTORY_LIMIT : MessageService.MAX_CHAT_HISTORY_LIMIT;
const safeLimit = Number.isFinite(limit) ? Math.min(Math.max(Math.trunc(limit), 1), ceiling) : 50;
return engine.getChatHistory(chatId, safeLimit, deep ? false : includeMedia);
}

// ========== Delete Message ==========
Expand Down
Loading