Spaces:
Runtime error
Runtime error
| import { Controller, Post, Get, Param, Body, Query, HttpCode, HttpStatus } from '@nestjs/common'; | |
| import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; | |
| import { MessageService } from './message.service'; | |
| import { BulkMessageService } from './bulk-message.service'; | |
| import { SendTextMessageDto, SendMediaMessageDto, SendAudioMessageDto, MessageResponseDto } from './dto'; | |
| import { SendTemplateMessageDto } from './dto/send-template.dto'; | |
| import { SendBulkMessageDto, BulkMessageResponseDto } from './dto/bulk-message.dto'; | |
| import { | |
| SendLocationDto, | |
| SendContactDto, | |
| SendPollDto, | |
| ReplyMessageDto, | |
| ForwardMessageDto, | |
| ReactMessageDto, | |
| DeleteMessageDto, | |
| EditMessageDto, | |
| } from './dto/message-actions.dto'; | |
| import { RequireRole } from '../auth/decorators/auth.decorators'; | |
| import { ApiKeyRole } from '../auth/entities/api-key.entity'; | |
| ('messages') | |
| ('sessions/:sessionId/messages') | |
| export class MessageController { | |
| constructor( | |
| private readonly messageService: MessageService, | |
| private readonly bulkMessageService: BulkMessageService, | |
| ) {} | |
| () | |
| ({ summary: 'Get message history for a session' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ name: 'chatId', required: false, description: 'Filter by chat ID' }) | |
| ({ | |
| name: 'from', | |
| required: false, | |
| description: 'Filter by sender. A phone also matches messages from a lid that resolves to it.', | |
| }) | |
| ({ name: 'limit', required: false, type: Number, description: 'Max messages to return (default 50)' }) | |
| ({ name: 'offset', required: false, type: Number, description: 'Offset for pagination' }) | |
| ({ | |
| status: 200, | |
| description: 'Message history', | |
| }) | |
| async getMessages( | |
| ('sessionId') sessionId: string, | |
| ('chatId') chatId?: string, | |
| ('from') from?: string, | |
| ('limit') limit?: string, | |
| ('offset') offset?: string, | |
| ) { | |
| return this.messageService.getMessages(sessionId, { | |
| chatId, | |
| from, | |
| limit: limit ? parseInt(limit, 10) : undefined, | |
| offset: offset ? parseInt(offset, 10) : undefined, | |
| }); | |
| } | |
| ('send-text') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Send a text message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Message sent', | |
| type: MessageResponseDto, | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Session not active or invalid request', | |
| }) | |
| ({ status: 404, description: 'Session not found' }) | |
| async sendText(('sessionId') sessionId: string, () dto: SendTextMessageDto): Promise<MessageResponseDto> { | |
| return this.messageService.sendText(sessionId, dto); | |
| } | |
| ('send-template') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Render a stored text template and send it as a text message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Template rendered and sent', | |
| type: MessageResponseDto, | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Session not active or invalid request', | |
| }) | |
| ({ status: 404, description: 'Session or template not found' }) | |
| async sendTemplate( | |
| ('sessionId') sessionId: string, | |
| () dto: SendTemplateMessageDto, | |
| ): Promise<MessageResponseDto> { | |
| return this.messageService.sendTemplate(sessionId, dto); | |
| } | |
| ('send-image') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Send an image message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Image sent', | |
| type: MessageResponseDto, | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Session not active or invalid request', | |
| }) | |
| async sendImage( | |
| ('sessionId') sessionId: string, | |
| () dto: SendMediaMessageDto, | |
| ): Promise<MessageResponseDto> { | |
| return this.messageService.sendImage(sessionId, dto); | |
| } | |
| ('send-video') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Send a video message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Video sent', | |
| type: MessageResponseDto, | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Session not active or invalid request', | |
| }) | |
| async sendVideo( | |
| ('sessionId') sessionId: string, | |
| () dto: SendMediaMessageDto, | |
| ): Promise<MessageResponseDto> { | |
| return this.messageService.sendVideo(sessionId, dto); | |
| } | |
| ('send-audio') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Send an audio/voice message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Audio sent', | |
| type: MessageResponseDto, | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Session not active or invalid request', | |
| }) | |
| async sendAudio( | |
| ('sessionId') sessionId: string, | |
| () dto: SendAudioMessageDto, | |
| ): Promise<MessageResponseDto> { | |
| return this.messageService.sendAudio(sessionId, dto); | |
| } | |
| ('send-document') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Send a document/file' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Document sent', | |
| type: MessageResponseDto, | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Session not active or invalid request', | |
| }) | |
| async sendDocument( | |
| ('sessionId') sessionId: string, | |
| () dto: SendMediaMessageDto, | |
| ): Promise<MessageResponseDto> { | |
| return this.messageService.sendDocument(sessionId, dto); | |
| } | |
| // ========== Phase 3: Extended Messaging ========== | |
| ('send-location') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Send a location message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Location sent', | |
| type: MessageResponseDto, | |
| }) | |
| async sendLocation(('sessionId') sessionId: string, () dto: SendLocationDto): Promise<MessageResponseDto> { | |
| return this.messageService.sendLocation(sessionId, dto); | |
| } | |
| ('send-contact') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Send a contact card message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Contact sent', | |
| type: MessageResponseDto, | |
| }) | |
| async sendContact(('sessionId') sessionId: string, () dto: SendContactDto): Promise<MessageResponseDto> { | |
| return this.messageService.sendContact(sessionId, dto); | |
| } | |
| ('send-sticker') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Send a sticker message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Sticker sent', | |
| type: MessageResponseDto, | |
| }) | |
| async sendSticker( | |
| ('sessionId') sessionId: string, | |
| () dto: SendMediaMessageDto, | |
| ): Promise<MessageResponseDto> { | |
| return this.messageService.sendSticker(sessionId, dto); | |
| } | |
| ('send-poll') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Send a native WhatsApp poll' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Poll sent', | |
| type: MessageResponseDto, | |
| }) | |
| async sendPoll(('sessionId') sessionId: string, () dto: SendPollDto): Promise<MessageResponseDto> { | |
| return this.messageService.sendPoll(sessionId, dto); | |
| } | |
| ('reply') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Reply to a message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Reply sent', | |
| type: MessageResponseDto, | |
| }) | |
| async reply(('sessionId') sessionId: string, () dto: ReplyMessageDto): Promise<MessageResponseDto> { | |
| return this.messageService.reply(sessionId, dto); | |
| } | |
| ('forward') | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Forward a message to another chat' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 201, | |
| description: 'Message forwarded', | |
| type: MessageResponseDto, | |
| }) | |
| async forward(('sessionId') sessionId: string, () dto: ForwardMessageDto): Promise<MessageResponseDto> { | |
| return this.messageService.forward(sessionId, dto); | |
| } | |
| // ========== Phase 3: Reactions ========== | |
| ('react') | |
| (HttpStatus.OK) | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Add or remove a reaction to a message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 200, | |
| description: 'Reaction added or removed. Send empty emoji to remove reaction.', | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Session not active or message not found', | |
| }) | |
| async react(('sessionId') sessionId: string, () dto: ReactMessageDto): Promise<{ success: boolean }> { | |
| await this.messageService.reactToMessage(sessionId, dto); | |
| return { success: true }; | |
| } | |
| (':chatId/history') | |
| ({ | |
| 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.', | |
| }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ name: 'chatId', description: 'Chat ID (e.g. 1234567890@c.us or groupId@g.us)' }) | |
| ({ name: 'limit', required: false, type: Number, description: 'Max messages to return (default 50)' }) | |
| ({ | |
| name: 'includeMedia', | |
| required: false, | |
| type: Boolean, | |
| description: 'When true, downloads media (base64) for messages that have it. Slower; default false.', | |
| }) | |
| ({ | |
| 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.', | |
| }) | |
| ({ status: 200, description: 'Chat history (most recent messages)' }) | |
| async getChatHistory( | |
| ('sessionId') sessionId: string, | |
| ('chatId') chatId: string, | |
| ('limit') limit?: string, | |
| ('includeMedia') includeMedia?: string, | |
| ('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 + 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', | |
| ); | |
| } | |
| (':chatId/:messageId/reactions') | |
| ({ summary: 'Get reactions for a specific message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ name: 'chatId', description: 'Chat ID containing the message' }) | |
| ({ name: 'messageId', description: 'Message ID to get reactions for' }) | |
| ({ | |
| status: 200, | |
| description: 'List of reactions with senders', | |
| }) | |
| async getReactions( | |
| ('sessionId') sessionId: string, | |
| ('chatId') chatId: string, | |
| ('messageId') messageId: string, | |
| ) { | |
| return this.messageService.getMessageReactions(sessionId, chatId, messageId); | |
| } | |
| // ========== Delete Message ========== | |
| ('delete') | |
| (HttpStatus.OK) | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Delete a message' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 200, | |
| description: 'Message deleted', | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Session not active or message not found', | |
| }) | |
| async deleteMessage( | |
| ('sessionId') sessionId: string, | |
| () dto: DeleteMessageDto, | |
| ): Promise<{ success: boolean }> { | |
| await this.messageService.deleteMessage(sessionId, dto); | |
| return { success: true }; | |
| } | |
| // ========== Edit Message ========== | |
| ('edit') | |
| (HttpStatus.OK) | |
| (ApiKeyRole.OPERATOR) | |
| ({ summary: 'Edit the text of a message sent by this account' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 200, | |
| description: 'Message edited', | |
| type: MessageResponseDto, | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Session not active, invalid request, or the send was blocked by a plugin', | |
| }) | |
| ({ | |
| status: 403, | |
| description: 'The message was not sent by this account, or the engine refused the edit', | |
| }) | |
| ({ status: 404, description: 'Message not found' }) | |
| async edit(('sessionId') sessionId: string, () dto: EditMessageDto): Promise<MessageResponseDto> { | |
| return this.messageService.editMessage(sessionId, dto); | |
| } | |
| // ========== Bulk Messaging ========== | |
| ('send-bulk') | |
| (ApiKeyRole.OPERATOR) | |
| (HttpStatus.ACCEPTED) | |
| ({ summary: 'Send messages to multiple recipients (async batch processing)' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ | |
| status: 202, | |
| description: 'Batch created and processing started', | |
| type: BulkMessageResponseDto, | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Session not active or invalid request', | |
| }) | |
| async sendBulk( | |
| ('sessionId') sessionId: string, | |
| () dto: SendBulkMessageDto, | |
| ): Promise<BulkMessageResponseDto> { | |
| const batch = await this.bulkMessageService.createBatch(sessionId, dto); | |
| const estimatedTime = new Date(Date.now() + batch.messages.length * (batch.options?.delayBetweenMessages || 3000)); | |
| return { | |
| batchId: batch.batchId, | |
| status: batch.status, | |
| totalMessages: batch.messages.length, | |
| estimatedCompletionTime: estimatedTime.toISOString(), | |
| statusUrl: `/api/sessions/${sessionId}/messages/batch/${batch.batchId}`, | |
| }; | |
| } | |
| ('batch/:batchId') | |
| ({ summary: 'Get batch processing status' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ name: 'batchId', description: 'Batch ID' }) | |
| ({ | |
| status: 200, | |
| description: 'Batch status and progress', | |
| }) | |
| ({ | |
| status: 404, | |
| description: 'Batch not found', | |
| }) | |
| async getBatchStatus(('sessionId') sessionId: string, ('batchId') batchId: string) { | |
| const batch = await this.bulkMessageService.getBatchStatus(sessionId, batchId); | |
| return { | |
| batchId: batch.batchId, | |
| status: batch.status, | |
| progress: batch.progress, | |
| results: batch.results, | |
| startedAt: batch.startedAt, | |
| completedAt: batch.completedAt, | |
| }; | |
| } | |
| ('batch/:batchId/cancel') | |
| (ApiKeyRole.OPERATOR) | |
| (HttpStatus.OK) | |
| ({ summary: 'Cancel a running batch' }) | |
| ({ name: 'sessionId', description: 'Session ID' }) | |
| ({ name: 'batchId', description: 'Batch ID' }) | |
| ({ | |
| status: 200, | |
| description: 'Batch cancelled', | |
| }) | |
| ({ | |
| status: 400, | |
| description: 'Batch already completed or cancelled', | |
| }) | |
| ({ | |
| status: 404, | |
| description: 'Batch not found', | |
| }) | |
| async cancelBatch(('sessionId') sessionId: string, ('batchId') batchId: string) { | |
| const batch = await this.bulkMessageService.cancelBatch(sessionId, batchId); | |
| return { | |
| batchId: batch.batchId, | |
| status: batch.status, | |
| progress: batch.progress, | |
| }; | |
| } | |
| } | |