diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ca12c1..7a7f22ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Added endpoint sendPresence * New Instance Manager * Added auto_create to the chatwoot set to create the inbox automatically or not +* Added reply, delete and message reaction in chatwoot v3.3.1 ### Fixed diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index b70cda9c..b83b2ddc 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -5,13 +5,18 @@ import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; +import { RepositoryBroker } from '../repository/repository.manager'; import { ChatwootService } from '../services/chatwoot.service'; import { waMonitor } from '../whatsapp.module'; const logger = new Logger('ChatwootController'); export class ChatwootController { - constructor(private readonly chatwootService: ChatwootService, private readonly configService: ConfigService) {} + constructor( + private readonly chatwootService: ChatwootService, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + ) {} public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { logger.verbose('requested createChatwoot from ' + instance.instanceName + ' instance'); @@ -87,7 +92,7 @@ export class ChatwootController { public async receiveWebhook(instance: InstanceDto, data: any) { logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); - const chatwootService = new ChatwootService(waMonitor, this.configService); + const chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); return chatwootService.receiveWebhook(instance, data); } diff --git a/src/whatsapp/models/message.model.ts b/src/whatsapp/models/message.model.ts index 252cd6e4..af5b7d0b 100644 --- a/src/whatsapp/models/message.model.ts +++ b/src/whatsapp/models/message.model.ts @@ -22,6 +22,7 @@ export class MessageRaw { source?: 'android' | 'web' | 'ios'; source_id?: string; source_reply_id?: string; + chatwootMessageId?: string; } const messageSchema = new Schema({ @@ -39,6 +40,7 @@ const messageSchema = new Schema({ source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] }, messageTimestamp: { type: Number, required: true }, owner: { type: String, required: true, minlength: 1 }, + chatwootMessageId: { type: String, required: false }, }); export const MessageModel = dbserver?.model(MessageRaw.name, messageSchema, 'messages'); diff --git a/src/whatsapp/repository/message.repository.ts b/src/whatsapp/repository/message.repository.ts index ed362815..e212ca3d 100644 --- a/src/whatsapp/repository/message.repository.ts +++ b/src/whatsapp/repository/message.repository.ts @@ -144,4 +144,55 @@ export class MessageRepository extends Repository { return []; } } + + public async update(data: MessageRaw[], instanceName: string, saveDb?: boolean): Promise { + try { + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('updating messages in db'); + + const messages = data.map((message) => { + return { + updateOne: { + filter: { 'key.id': message.key.id }, + update: { ...message }, + }, + }; + }); + + const { nModified } = await this.messageModel.bulkWrite(messages); + + this.logger.verbose('messages updated in db: ' + nModified + ' messages'); + return { insertCount: nModified }; + } + + this.logger.verbose('updating messages in store'); + + const store = this.configService.get('STORE'); + + if (store.MESSAGES) { + this.logger.verbose('updating messages in store'); + data.forEach((message) => { + this.writeStore({ + path: join(this.storePath, 'messages', instanceName), + fileName: message.key.id, + data: message, + }); + this.logger.verbose( + 'messages updated in store in path: ' + + join(this.storePath, 'messages', instanceName) + + '/' + + message.key.id, + ); + }); + + this.logger.verbose('messages updated in store: ' + data.length + ' messages'); + return { insertCount: data.length }; + } + + this.logger.verbose('messages not updated'); + return { insertCount: 0 }; + } catch (error) { + this.logger.error(error); + } + } } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index b2fbb879..352b7e0e 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -11,7 +11,9 @@ import { Logger } from '../../config/logger.config'; import { ROOT_DIR } from '../../config/path.config'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; -import { SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; +import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; +import { MessageRaw } from '../models'; +import { RepositoryBroker } from '../repository/repository.manager'; import { WAMonitoringService } from './monitor.service'; export class ChatwootService { @@ -22,7 +24,11 @@ export class ChatwootService { private provider: any; - constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) { + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + ) { this.messageCache = new Set(); } @@ -640,6 +646,7 @@ export class ChatwootService { encoding: string; filename: string; }[], + messageBody?: any, ) { this.logger.verbose('create message to instance: ' + instance.instanceName); @@ -650,6 +657,8 @@ export class ChatwootService { return null; } + const replyToIds = await this.getReplyToIds(messageBody, instance); + this.logger.verbose('create message in chatwoot'); const message = await client.messages.create({ accountId: this.provider.account_id, @@ -659,6 +668,9 @@ export class ChatwootService { message_type: messageType, attachments: attachments, private: privateMessage || false, + content_attributes: { + ...replyToIds, + }, }, }); @@ -754,6 +766,8 @@ export class ChatwootService { file: string, messageType: 'incoming' | 'outgoing' | undefined, content?: string, + instance?: InstanceDto, + messageBody?: any, ) { this.logger.verbose('send data to chatwoot'); @@ -770,6 +784,16 @@ export class ChatwootService { this.logger.verbose('temp file found'); data.append('attachments[]', createReadStream(file)); + if (messageBody && instance) { + const replyToIds = await this.getReplyToIds(messageBody, instance); + + if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { + data.append('content_attributes', { + ...replyToIds, + }); + } + } + this.logger.verbose('get client to instance: ' + this.provider.instanceName); const config = { method: 'post', @@ -890,7 +914,7 @@ export class ChatwootService { } } - public async sendAttachment(waInstance: any, number: string, media: any, caption?: string) { + public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) { this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); try { @@ -932,13 +956,14 @@ export class ChatwootService { options: { delay: 1200, presence: 'recording', + ...options, }, }; - await waInstance?.audioWhatsapp(data, true); + const messageSent = await waInstance?.audioWhatsapp(data, true); this.logger.verbose('audio sent'); - return; + return messageSent; } this.logger.verbose('send media to instance: ' + waInstance.instanceName); @@ -952,6 +977,7 @@ export class ChatwootService { options: { delay: 1200, presence: 'composing', + ...options, }, }; @@ -960,10 +986,10 @@ export class ChatwootService { data.mediaMessage.caption = caption; } - await waInstance?.mediaMessage(data, true); + const messageSent = await waInstance?.mediaMessage(data, true); this.logger.verbose('media sent'); - return; + return messageSent; } catch (error) { this.logger.error(error); } @@ -982,7 +1008,13 @@ export class ChatwootService { } this.logger.verbose('check if is bot'); - if (!body?.conversation || body.private || body.event === 'message_updated') return { message: 'bot' }; + if ( + !body?.conversation || + body.private || + (body.event === 'message_updated' && !body.content_attributes?.deleted) + ) { + return { message: 'bot' }; + } this.logger.verbose('check if is group'); const chatId = @@ -991,6 +1023,21 @@ export class ChatwootService { const senderName = body?.sender?.name; const waInstance = this.waMonitor.waInstances[instance.instanceName]; + this.logger.verbose('check if is a message deletion'); + if (body.event === 'message_updated' && body.content_attributes?.deleted) { + const message = await this.repository.message.find({ + where: { + owner: instance.instanceName, + chatwootMessageId: body.id, + }, + limit: 1, + }); + if (message.length && message[0].key?.id) { + await waInstance?.client.sendMessage(message[0].key.remoteJid, { delete: message[0].key }); + } + return { message: 'bot' }; + } + if (chatId === '123456' && body.message_type === 'outgoing') { this.logger.verbose('check if is command'); @@ -1082,7 +1129,26 @@ export class ChatwootService { formatText = null; } - await this.sendAttachment(waInstance, chatId, attachment.data_url, formatText); + const options: Options = { + quoted: await this.getQuotedMessage(body, instance), + }; + + const messageSent = await this.sendAttachment( + waInstance, + chatId, + attachment.data_url, + formatText, + options, + ); + + this.updateChatwootMessageId( + { + ...messageSent, + owner: instance.instanceName, + }, + body.id, + instance, + ); } } else { this.logger.verbose('message is text'); @@ -1096,10 +1162,20 @@ export class ChatwootService { options: { delay: 1200, presence: 'composing', + quoted: await this.getQuotedMessage(body, instance), }, }; - await waInstance?.textMessage(data, true); + const messageSent = await waInstance?.textMessage(data, true); + + this.updateChatwootMessageId( + { + ...messageSent, + owner: instance.instanceName, + }, + body.id, + instance, + ); } } } @@ -1131,6 +1207,65 @@ export class ChatwootService { } } + private updateChatwootMessageId(message: MessageRaw, chatwootMessageId: string, instance: InstanceDto) { + if (!chatwootMessageId) { + return; + } + message.chatwootMessageId = chatwootMessageId; + this.repository.message.update([message], instance.instanceName, true); + } + + private async getReplyToIds( + msg: any, + instance: InstanceDto, + ): Promise<{ in_reply_to: string; in_reply_to_external_id: string }> { + let inReplyTo = null; + let inReplyToExternalId = null; + + if (msg) { + inReplyToExternalId = msg.message?.extendedTextMessage?.contextInfo?.stanzaId; + if (inReplyToExternalId) { + const message = await this.repository.message.find({ + where: { + key: { + id: inReplyToExternalId, + }, + owner: instance.instanceName, + }, + limit: 1, + }); + if (message.length && message[0]?.chatwootMessageId) { + inReplyTo = message[0].chatwootMessageId; + } + } + } + + return { + in_reply_to: inReplyTo, + in_reply_to_external_id: inReplyToExternalId, + }; + } + + private async getQuotedMessage(msg: any, instance: InstanceDto): Promise { + if (msg?.content_attributes?.in_reply_to) { + const message = await this.repository.message.find({ + where: { + chatwootMessageId: msg?.content_attributes?.in_reply_to, + owner: instance.instanceName, + }, + limit: 1, + }); + if (message.length && message[0]?.key?.id) { + return { + key: message[0].key, + message: message[0].message, + }; + } + } + + return null; + } + private isMediaMessage(message: any) { this.logger.verbose('check if is media message'); const media = [ @@ -1164,6 +1299,18 @@ export class ChatwootService { return adsMessage; } + private getReactionMessage(msg: any) { + interface ReactionMessage { + key: MessageRaw['key']; + text: string; + } + const reactionMessage: ReactionMessage | undefined = msg?.reactionMessage; + + this.logger.verbose('Get reaction message if it exists'); + reactionMessage && this.logger.verbose('Reaction message: ' + reactionMessage); + return reactionMessage; + } + private getTypeMessage(msg: any) { this.logger.verbose('get type message'); @@ -1319,7 +1466,9 @@ export class ChatwootService { const adsMessage = this.getAdsMessage(body.message); - if (!bodyMessage && !isMedia) { + const reactionMessage = this.getReactionMessage(body.message); + + if (!bodyMessage && !isMedia && !reactionMessage) { this.logger.warn('no body message found'); return; } @@ -1378,7 +1527,7 @@ export class ChatwootService { } this.logger.verbose('send data to chatwoot'); - const send = await this.sendData(getConversation, fileName, messageType, content); + const send = await this.sendData(getConversation, fileName, messageType, content, instance, body); if (!send) { this.logger.warn('message not sent'); @@ -1399,7 +1548,7 @@ export class ChatwootService { this.logger.verbose('message is not group'); this.logger.verbose('send data to chatwoot'); - const send = await this.sendData(getConversation, fileName, messageType, bodyMessage); + const send = await this.sendData(getConversation, fileName, messageType, bodyMessage, instance, body); if (!send) { this.logger.warn('message not sent'); @@ -1419,6 +1568,35 @@ export class ChatwootService { } } + this.logger.verbose('check if has ReactionMessage'); + if (reactionMessage) { + this.logger.verbose('send data to chatwoot'); + if (reactionMessage.text) { + const send = await this.createMessage( + instance, + getConversation, + reactionMessage.text, + messageType, + false, + [], + { + message: { extendedTextMessage: { contextInfo: { stanzaId: reactionMessage.key.id } } }, + }, + ); + if (!send) { + this.logger.warn('message not sent'); + return; + } + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + this.messageCache = this.loadMessageCache(); + this.messageCache.add(send.id.toString()); + this.logger.verbose('save message cache'); + this.saveMessageCache(); + } + + return; + } + this.logger.verbose('check if has Ads Message'); if (adsMessage) { this.logger.verbose('message is from Ads'); @@ -1461,6 +1639,8 @@ export class ChatwootService { fileName, messageType, `${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`, + instance, + body, ); if (!send) { @@ -1496,7 +1676,7 @@ export class ChatwootService { } this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversation, content, messageType); + const send = await this.createMessage(instance, getConversation, content, messageType, false, [], body); if (!send) { this.logger.warn('message not sent'); @@ -1517,7 +1697,7 @@ export class ChatwootService { this.logger.verbose('message is not group'); this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversation, bodyMessage, messageType); + const send = await this.createMessage(instance, getConversation, bodyMessage, messageType, false, [], body); if (!send) { this.logger.warn('message not sent'); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index f2e6d811..73cbfdf6 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -166,7 +166,7 @@ export class WAStartupService { private phoneNumber: string; - private chatwootService = new ChatwootService(waMonitor, this.configService); + private chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); private typebotService = new TypebotService(waMonitor, this.configService); @@ -1778,11 +1778,15 @@ export class WAStartupService { this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( + const chatwootSentMessage = await this.chatwootService.eventWhatsapp( Events.MESSAGES_UPSERT, { instanceName: this.instance.name }, messageRaw, ); + + if (chatwootSentMessage?.id) { + messageRaw.chatwootMessageId = chatwootSentMessage.id; + } } const typebotSessionRemoteJid = this.localTypebot.sessions?.find( diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index d64d2532..1fb84de6 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -129,9 +129,9 @@ const sqsService = new SqsService(waMonitor); export const sqsController = new SqsController(sqsService); -const chatwootService = new ChatwootService(waMonitor, configService); +const chatwootService = new ChatwootService(waMonitor, configService, repository); -export const chatwootController = new ChatwootController(chatwootService, configService); +export const chatwootController = new ChatwootController(chatwootService, configService, repository); const settingsService = new SettingsService(waMonitor);