Skip to content
Open
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
36 changes: 36 additions & 0 deletions src/engine/adapters/whatsapp-web-js.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IncomingMessage[]> {
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<void> {
this.ensureReady();
Expand Down
1 change: 1 addition & 0 deletions src/engine/interfaces/whatsapp-engine.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ export interface IWhatsAppEngine {

// Message Operations
deleteMessage(chatId: string, messageId: string, forEveryone?: boolean): Promise<void>;
getChatHistory(chatId: string, limit?: number, includeMedia?: boolean): Promise<IncomingMessage[]>;

// Contact Extended Operations
getProfilePicture(contactId: string): Promise<string | null>;
Expand Down
31 changes: 31 additions & 0 deletions src/modules/message/message.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down
25 changes: 25 additions & 0 deletions src/modules/message/message.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function createMockEngine() {
reactToMessage: jest.fn().mockResolvedValue(undefined),
getMessageReactions: jest.fn().mockResolvedValue([]),
deleteMessage: jest.fn().mockResolvedValue(undefined),
getChatHistory: jest.fn().mockResolvedValue([]),
};
}

Expand Down Expand Up @@ -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', {
Expand Down
10 changes: 10 additions & 0 deletions src/modules/message/message.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down