feat: added reply, delete and message reaction in chatwoot v3.3.1

This commit is contained in:
Davidson Gomes 2023-12-12 15:16:09 -03:00
parent 3e904aa160
commit ae66be197e
7 changed files with 264 additions and 21 deletions

View File

@ -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

View File

@ -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);
}

View File

@ -22,6 +22,7 @@ export class MessageRaw {
source?: 'android' | 'web' | 'ios';
source_id?: string;
source_reply_id?: string;
chatwootMessageId?: string;
}
const messageSchema = new Schema<MessageRaw>({
@ -39,6 +40,7 @@ const messageSchema = new Schema<MessageRaw>({
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');

View File

@ -144,4 +144,55 @@ export class MessageRepository extends Repository {
return [];
}
}
public async update(data: MessageRaw[], instanceName: string, saveDb?: boolean): Promise<IInsert> {
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<StoreConf>('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);
}
}
}

View File

@ -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<Quoted> {
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');

View File

@ -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(

View File

@ -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);