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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Plugins can read recent chat history** via a new `ctx.engine.getChatHistory(sessionId, chatId, limit?, includeMedia?)` capability, gated by the `engine:read` permission and the plugin's active-session scope like the other engine reads. The limit is clamped host-side (max 100), and both message directions are returned. This is the host-side prerequisite for an adapter to backfill prior conversation context. (#609)

## [0.8.4] - 2026-07-03

### Added
Expand Down
25 changes: 25 additions & 0 deletions src/core/plugins/plugin-capability.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,31 @@ describe('PluginLoaderService capability facade — ctx.engine', () => {
await ctx.engine.getGroupInfo('sess-1', 'g@g.us');
expect(engine.getGroupInfo).toHaveBeenCalledWith('g@g.us');
});

it('engine.getChatHistory delegates to the engine and clamps the limit to 100', async () => {
const engine = { getChatHistory: jest.fn().mockResolvedValue([]) };
build(engine);
const ctx = contextFor(makePlugin(['*'], ['engine:read']));
await ctx.engine.getChatHistory('sess-1', 'c@c.us', 500, true);
expect(engine.getChatHistory).toHaveBeenCalledWith('c@c.us', 100, true); // 500 clamped to 100
});

it('engine.getChatHistory defaults the limit and clamps a non-positive value to 1', async () => {
const engine = { getChatHistory: jest.fn().mockResolvedValue([]) };
build(engine);
const ctx = contextFor(makePlugin(['*'], ['engine:read']));
await ctx.engine.getChatHistory('sess-1', 'c@c.us'); // no limit → default 50, includeMedia → false
await ctx.engine.getChatHistory('sess-1', 'c@c.us', 0);
expect(engine.getChatHistory).toHaveBeenNthCalledWith(1, 'c@c.us', 50, false);
expect(engine.getChatHistory).toHaveBeenNthCalledWith(2, 'c@c.us', 1, false);
});

it('denies engine.getChatHistory without the engine:read permission', async () => {
const { sessionService } = build({ getChatHistory: jest.fn() });
const ctx = contextFor(makePlugin(['*'], ['messages:send']));
await expect(ctx.engine.getChatHistory('sess-1', 'c@c.us')).rejects.toBeInstanceOf(PluginCapabilityError);
expect(sessionService.getEngine).not.toHaveBeenCalled();
});
});

describe('PluginLoaderService capability facade — ctx.net', () => {
Expand Down
8 changes: 8 additions & 0 deletions src/core/plugins/plugin-loader.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,14 @@ export class PluginLoaderService implements OnModuleInit, OnModuleDestroy {
checkNumberExists: async (sessionId, phone) =>
this.resolveEngineRead(plugin, sessionId).checkNumberExists(phone),
getChats: async sessionId => this.resolveEngineRead(plugin, sessionId).getChats(),
getChatHistory: async (sessionId, chatId, limit, includeMedia) =>
this.resolveEngineRead(plugin, sessionId).getChatHistory(
chatId,
// Clamp to the REST non-deep ceiling (MessageService.MAX_CHAT_HISTORY_LIMIT = 100) so an
// untrusted plugin can't request an unbounded history fetch.
Math.min(Math.max(Math.trunc(limit ?? 50), 1), 100),
includeMedia ?? false,
),
} satisfies PluginEngineReadCapability,
net: {
fetch: async (url, init) => {
Expand Down
7 changes: 7 additions & 0 deletions src/core/plugins/plugin.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,13 @@ export interface PluginEngineReadCapability {
getContactById(sessionId: string, contactId: string): ReturnType<IWhatsAppEngine['getContactById']>;
checkNumberExists(sessionId: string, phone: string): ReturnType<IWhatsAppEngine['checkNumberExists']>;
getChats(sessionId: string): ReturnType<IWhatsAppEngine['getChats']>;
/** Recent messages for a chat (both directions), for history backfill. `limit` is clamped host-side. */
getChatHistory(
sessionId: string,
chatId: string,
limit?: number,
includeMedia?: boolean,
): ReturnType<IWhatsAppEngine['getChatHistory']>;
}

/** Outbound HTTP for a plugin — always through the host SSRF guard, scoped to `manifest.net.allow`. */
Expand Down
Loading