diff --git a/src/engine/adapters/whatsapp-web-js.adapter.ts b/src/engine/adapters/whatsapp-web-js.adapter.ts index 727a3db..84a0bd0 100644 --- a/src/engine/adapters/whatsapp-web-js.adapter.ts +++ b/src/engine/adapters/whatsapp-web-js.adapter.ts @@ -774,6 +774,42 @@ export class WhatsAppWebJsAdapter extends EventEmitter implements IWhatsAppEngin // ========== Gap Quick Wins Implementation ========== + async getChatHistory(chatId: string, limit: number = 50, includeMedia: boolean = false): Promise { + this.ensureReady(); + const chat = await this.client!.getChatById(chatId); + const messages = await chat.fetchMessages({ limit }); + const results: IncomingMessage[] = []; + for (const msg of messages) { + const out: IncomingMessage = { + id: msg.id._serialized, + from: msg.from, + to: msg.to, + chatId, + body: msg.body, + type: msg.type, + timestamp: msg.timestamp, + fromMe: msg.fromMe, + isGroup: chatId.endsWith('@g.us'), + }; + if (includeMedia && msg.hasMedia) { + try { + const media = await msg.downloadMedia(); + if (media) { + out.media = { + mimetype: media.mimetype, + filename: media.filename || undefined, + data: media.data, + }; + } + } catch (error) { + this.logger.warn(`Failed to download media for ${msg.id._serialized}: ${String(error)}`); + } + } + results.push(out); + } + return results; + } + // Delete Message async deleteMessage(chatId: string, messageId: string, forEveryone: boolean = true): Promise { this.ensureReady(); diff --git a/src/engine/interfaces/whatsapp-engine.interface.ts b/src/engine/interfaces/whatsapp-engine.interface.ts index f3e5de6..3c5f059 100644 --- a/src/engine/interfaces/whatsapp-engine.interface.ts +++ b/src/engine/interfaces/whatsapp-engine.interface.ts @@ -259,6 +259,7 @@ export interface IWhatsAppEngine { // Message Operations deleteMessage(chatId: string, messageId: string, forEveryone?: boolean): Promise; + getChatHistory(chatId: string, limit?: number, includeMedia?: boolean): Promise; // Contact Extended Operations getProfilePicture(contactId: string): Promise; diff --git a/src/modules/message/message.controller.ts b/src/modules/message/message.controller.ts index 8c84eed..ea96a54 100644 --- a/src/modules/message/message.controller.ts +++ b/src/modules/message/message.controller.ts @@ -240,6 +240,37 @@ export class MessageController { return { success: true }; } + @Get(':chatId/history') + @ApiOperation({ + summary: 'Fetch chat history live from WhatsApp', + description: + 'Reads messages directly from the WhatsApp client for the given chat, bypassing the local DB. ' + + 'Useful for retrieving messages that arrived before the gateway was started.', + }) + @ApiParam({ name: 'sessionId', description: 'Session ID' }) + @ApiParam({ name: 'chatId', description: 'Chat ID (e.g. 1234567890@c.us or groupId@g.us)' }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Max messages to return (default 50)' }) + @ApiQuery({ + name: 'includeMedia', + required: false, + type: Boolean, + description: 'When true, downloads media (base64) for messages that have it. Slower; 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, + ) { + return this.messageService.getChatHistory( + sessionId, + chatId, + limit ? parseInt(limit, 10) : undefined, + includeMedia === 'true' || includeMedia === '1', + ); + } + @Get(':chatId/:messageId/reactions') @ApiOperation({ summary: 'Get reactions for a specific message' }) @ApiParam({ name: 'sessionId', description: 'Session ID' }) diff --git a/src/modules/message/message.service.spec.ts b/src/modules/message/message.service.spec.ts index ad8d57a..f639793 100644 --- a/src/modules/message/message.service.spec.ts +++ b/src/modules/message/message.service.spec.ts @@ -24,6 +24,7 @@ function createMockEngine() { reactToMessage: jest.fn().mockResolvedValue(undefined), getMessageReactions: jest.fn().mockResolvedValue([]), deleteMessage: jest.fn().mockResolvedValue(undefined), + getChatHistory: jest.fn().mockResolvedValue([]), }; } @@ -345,6 +346,30 @@ describe('MessageService', () => { }); }); + describe('getChatHistory', () => { + it('should call engine.getChatHistory with default limit and includeMedia=false', async () => { + await service.getChatHistory('sess-1', 'test@c.us'); + expect(mockEngine.getChatHistory).toHaveBeenCalledWith('test@c.us', 50, false); + }); + + it('should pass through custom limit', async () => { + await service.getChatHistory('sess-1', 'test@c.us', 10); + expect(mockEngine.getChatHistory).toHaveBeenCalledWith('test@c.us', 10, false); + }); + + it('should pass through includeMedia flag', async () => { + await service.getChatHistory('sess-1', 'test@c.us', 5, true); + expect(mockEngine.getChatHistory).toHaveBeenCalledWith('test@c.us', 5, true); + }); + + it('should return engine result', async () => { + const fake = [{ id: 'm1', body: 'hi', from: 'a', to: 'b', chatId: 'test@c.us' }]; + mockEngine.getChatHistory.mockResolvedValueOnce(fake); + const result = await service.getChatHistory('sess-1', 'test@c.us'); + expect(result).toBe(fake); + }); + }); + describe('deleteMessage', () => { it('should call engine.deleteMessage with forEveryone default true', async () => { await service.deleteMessage('sess-1', { diff --git a/src/modules/message/message.service.ts b/src/modules/message/message.service.ts index cd245bd..a7a2ac0 100644 --- a/src/modules/message/message.service.ts +++ b/src/modules/message/message.service.ts @@ -456,6 +456,16 @@ export class MessageService { return engine.getMessageReactions(chatId, messageId); } + /** + * 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. + */ + async getChatHistory(sessionId: string, chatId: string, limit = 50, includeMedia = false) { + const engine = this.getEngine(sessionId); + return engine.getChatHistory(chatId, limit, includeMedia); + } + // ========== Delete Message ========== async deleteMessage(