diff --git a/CHANGELOG.md b/CHANGELOG.md index 3809d59b..d960aa9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ### Features * Added messages.delete event +* Added restart instance endpoint +* Created automation for creating instances in the chatwoot bot with the command '#inbox_whatsapp:' +* Change Baileys version to: 6.4.0 +* Send contact in chatwoot ### Fixed @@ -12,7 +16,14 @@ * Added validations in create instance * Removed link preview endpoint, now it's done automatically from sending conventional text * Added group membership validation before sending message to groups -* Adjusts in Dockerfile +* Adjusts in docker files +* Adjusts in returns in endpoints chatwoot and webhook +* Fixed ghost mentions in send text message +* Fixed bug that saved contacts from groups came without number in chatwoot +* Fixed problem to receive csat in chatwoot +* Fixed require fileName for document only in base64 for send media message +* Bug fix when sending mobile message change contact name to number in chatwoot +* Bug fix when connecting whatsapp does not send confirmation message # 1.2.2 (2023-07-15 09:36) diff --git a/Docker/mongodb/docker-compose.yaml b/Docker/mongodb/docker-compose.yaml index de1b7e2b..698ca50c 100644 --- a/Docker/mongodb/docker-compose.yaml +++ b/Docker/mongodb/docker-compose.yaml @@ -37,6 +37,5 @@ volumes: networks: evolution-net: - name: evolution-net external: true \ No newline at end of file diff --git a/Docker/redis/docker-compose.yaml b/Docker/redis/docker-compose.yaml index dce371f2..6409b851 100644 --- a/Docker/redis/docker-compose.yaml +++ b/Docker/redis/docker-compose.yaml @@ -25,5 +25,4 @@ volumes: networks: evolution-net: - name: evolution-net external: true diff --git a/docker-compose-full.yaml b/docker-compose-full.yaml deleted file mode 100644 index 9b108101..00000000 --- a/docker-compose-full.yaml +++ /dev/null @@ -1,73 +0,0 @@ -version: '3.3' - -services: - redis: - image: redis:latest - container_name: redis - ports: - - 6379:6379 - - rebrow: - image: marian/rebrow - ports: - - 5001:5001 - links: - - redis - - mongodb: - container_name: mongodb - image: mongo - restart: always - volumes: - - evolution_mongodb_data:/data/db - - evolution_mongodb_configdb:/data/configdb - ports: - - 27017:27017 - environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: root - expose: - - 27017 - - mongo-express: - image: mongo-express - environment: - ME_CONFIG_BASICAUTH_USERNAME: root - ME_CONFIG_BASICAUTH_PASSWORD: root - ME_CONFIG_MONGODB_SERVER: mongodb - ME_CONFIG_MONGODB_ADMINUSERNAME: root - ME_CONFIG_MONGODB_ADMINPASSWORD: root - ports: - - 8081:8081 - links: - - mongodb - api: - container_name: evolution_api - image: evolution/api:local - restart: always - ports: - - 8080:8080 - volumes: - - evolution_instances:/evolution/instances - - evolution_store:/evolution/store - env_file: - - ./Docker/.env - command: ['node', './dist/src/main.js'] - expose: - - 8080 - links: - - mongodb - - redis - -volumes: - evolution_instances: - evolution_store: - evolution_mongodb_data: - evolution_mongodb_configdb: - evolution_redis: - -networks: - default: - external: - name: evolution-net - \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 51f5bd43..c6d1bc73 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,6 +22,5 @@ volumes: networks: evolution-net: - name: evolution-net external: true \ No newline at end of file diff --git a/package.json b/package.json index 5feef990..5c182838 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "@ffmpeg-installer/ffmpeg": "^1.1.0", "@figuro/chatwoot-sdk": "^1.1.14", "@hapi/boom": "^10.0.1", - "@whiskeysockets/baileys": "github:DavidsonGomes/Baileys", + "@sentry/node": "^7.59.2", + "@whiskeysockets/baileys": "^6.4.0", "axios": "^1.3.5", "class-validator": "^0.13.2", "compression": "^1.7.4", diff --git a/src/main.ts b/src/main.ts index 7d16b5c6..ac66e7b5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import { waMonitor } from './whatsapp/whatsapp.module'; import { HttpStatus, router } from './whatsapp/routers/index.router'; import 'express-async-errors'; import { ServerUP } from './utils/server-up'; +import * as Sentry from '@sentry/node'; function initWA() { waMonitor.loadInstance(); @@ -19,6 +20,27 @@ function bootstrap() { const logger = new Logger('SERVER'); const app = express(); + // Sentry.init({ + // dsn: '', + // integrations: [ + // // enable HTTP calls tracing + // new Sentry.Integrations.Http({ tracing: true }), + // // enable Express.js middleware tracing + // new Sentry.Integrations.Express({ app }), + // // Automatically instrument Node.js libraries and frameworks + // ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), + // ], + + // // Set tracesSampleRate to 1.0 to capture 100% + // // of transactions for performance monitoring. + // // We recommend adjusting this value in production + // tracesSampleRate: 1.0, + // }); + + // app.use(Sentry.Handlers.requestHandler()); + + // app.use(Sentry.Handlers.tracingHandler()); + app.use( cors({ origin(requestOrigin, callback) { @@ -43,6 +65,13 @@ function bootstrap() { app.use('/', router); + // app.use(Sentry.Handlers.errorHandler()); + + // app.use(function onError(err, req, res, next) { + // res.statusCode = 500; + // res.end(res.sentry + '\n'); + // }); + app.use( (err: Error, req: Request, res: Response, next: NextFunction) => { if (err) { diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index a4367833..de0aef7a 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -66,6 +66,18 @@ export class ChatwootController { const urlServer = this.configService.get('SERVER').URL; + if (Object.keys(result).length === 0) { + return { + enabled: false, + url: '', + account_id: '', + token: '', + sign_msg: false, + name_inbox: '', + webhook_url: '', + }; + } + const response = { ...result, webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, @@ -78,7 +90,7 @@ export class ChatwootController { logger.verbose( 'requested receiveWebhook from ' + instance.instanceName + ' instance', ); - const chatwootService = new ChatwootService(waMonitor); + const chatwootService = new ChatwootService(waMonitor, this.configService); return chatwootService.receiveWebhook(instance, data); } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 8bb35e35..f0adb3a3 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -64,7 +64,10 @@ export class InstanceController { this.repository, this.cache, ); - instance.instanceName = instanceName; + instance.instanceName = instanceName + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + .replace(' ', ''); this.logger.verbose('instance: ' + instance.instanceName + ' created'); this.waMonitor.waInstances[instance.instanceName] = instance; @@ -191,7 +194,10 @@ export class InstanceController { this.repository, this.cache, ); - instance.instanceName = instanceName; + instance.instanceName = instanceName + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + .replace(' ', ''); this.logger.verbose('instance: ' + instance.instanceName + ' created'); @@ -357,24 +363,8 @@ export class InstanceController { try { this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); - this.logger.verbose('deleting instance: ' + instanceName); - delete this.waMonitor.waInstances[instanceName]; - - this.logger.verbose('creating instance: ' + instanceName); - const instance = new WAStartupService( - this.configService, - this.eventEmitter, - this.repository, - this.cache, - ); - - instance.instanceName = instanceName; - - this.logger.verbose('instance: ' + instance.instanceName + ' created'); - - this.logger.verbose('connecting instance: ' + instanceName); - await instance.connectToWhatsapp(); - this.waMonitor.waInstances[instance.instanceName] = instance; + this.logger.verbose('logging out instance: ' + instanceName); + this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); return { error: false, message: 'Instance restarted' }; } catch (error) { diff --git a/src/whatsapp/controllers/sendMessage.controller.ts b/src/whatsapp/controllers/sendMessage.controller.ts index e80cbfcf..fb942a9c 100644 --- a/src/whatsapp/controllers/sendMessage.controller.ts +++ b/src/whatsapp/controllers/sendMessage.controller.ts @@ -30,9 +30,15 @@ export class SendMessageController { public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto) { logger.verbose('requested sendMedia from ' + instanceName + ' instance'); - if (isBase64(data?.mediaMessage?.media) && !data?.mediaMessage?.fileName) { - throw new BadRequestException('For bse64 the file name must be informed.'); + + if ( + isBase64(data?.mediaMessage?.media) && + !data?.mediaMessage?.fileName && + data?.mediaMessage?.mediatype === 'document' + ) { + throw new BadRequestException('For base64 the file name must be informed.'); } + logger.verbose( 'isURL: ' + isURL(data?.mediaMessage?.media) + diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 384cedd8..2ccf0671 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -12,6 +12,7 @@ import mimeTypes from 'mime-types'; import { SendAudioDto } from '../dto/sendMessage.dto'; import { SendMediaDto } from '../dto/sendMessage.dto'; import { ROOT_DIR } from '../../config/path.config'; +import { ConfigService, HttpServer } from '../../config/env.config'; export class ChatwootService { private messageCacheFile: string; @@ -21,7 +22,10 @@ export class ChatwootService { private provider: any; - constructor(private readonly waMonitor: WAMonitoringService) { + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + ) { this.messageCache = new Set(); } @@ -433,7 +437,7 @@ export class ChatwootService { instance, body.key.participant.split('@')[0], filterInbox.id, - isGroup, + false, body.pushName, picture_url.profilePictureUrl || null, ); @@ -449,21 +453,24 @@ export class ChatwootService { const findContact = await this.findContact(instance, chatId); let contact: any; - - if (findContact) { - contact = await this.updateContact(instance, findContact.id, { - name: nameContact, - avatar_url: picture_url.profilePictureUrl || null, - }); + if (body.key.fromMe) { + contact = findContact; } else { - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - ); + if (findContact) { + contact = await this.updateContact(instance, findContact.id, { + name: nameContact, + avatar_url: picture_url.profilePictureUrl || null, + }); + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } } if (!contact) { @@ -471,7 +478,8 @@ export class ChatwootService { return null; } - const contactId = contact.payload.id || contact.payload.contact.id; + const contactId = + contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { this.logger.verbose('update contact name in chatwoot'); @@ -981,6 +989,33 @@ export class ChatwootService { await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); await waInstance?.client?.ws?.close(); } + + if (command.includes('#inbox_whatsapp')) { + const urlServer = this.configService.get('SERVER').URL; + const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + const data = { + instanceName: command.split(':')[1], + qrcode: true, + chatwoot_account_id: this.provider.account_id, + chatwoot_token: this.provider.token, + chatwoot_url: this.provider.url, + chatwoot_sign_msg: this.provider.sign_msg, + }; + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${urlServer}/instance/create`, + headers: { + 'Content-Type': 'application/json', + apikey: apiKey, + }, + data: data, + }; + + await axios.request(config); + } } if ( @@ -1059,7 +1094,11 @@ export class ChatwootService { } } - if (body.message_type === 'template' && body.content_type === 'input_csat') { + if ( + body.message_type === 'template' && + body.content_type === 'input_csat' && + body.event === 'message_created' + ) { this.logger.verbose('check if is csat'); const data: SendTextDto = { @@ -1119,6 +1158,7 @@ export class ChatwootService { documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, audioMessage: msg.audioMessage?.caption, + contactMessage: msg.contactMessage?.vcard, }; this.logger.verbose('type message: ' + types); @@ -1132,6 +1172,25 @@ export class ChatwootService { const result = typeKey ? types[typeKey] : undefined; + if (typeKey === 'contactMessage') { + const vCardData = result.split('\n'); + const contactInfo = {}; + + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); + + const formattedContact = `**Contact:** + **name:** ${contactInfo['FN']} + **number:** ${contactInfo['item1.TEL;waid=5511952801378']}`; + + this.logger.verbose('message content: ' + formattedContact); + return formattedContact; + } + this.logger.verbose('message content: ' + result); return result; @@ -1142,8 +1201,12 @@ export class ChatwootService { const types = this.getTypeMessage(msg); + console.log('types', types); + const messageContent = this.getMessageContent(types); + console.log('messageContent', messageContent); + this.logger.verbose('conversation message: ' + messageContent); return messageContent; @@ -1394,37 +1457,38 @@ export class ChatwootService { if (event === 'connection.update') { this.logger.verbose('event connection.update'); - if (body.state === 'open') { - const msgConnection = `πŸš€ ConexΓ£o realizada com sucesso!`; + console.log('connection.update', body); + if (body.status === 'open') { + const msgConnection = `πŸš€ ConexΓ£o estabelecida com sucesso!`; this.logger.verbose('send message to chatwoot'); await this.createBotMessage(instance, msgConnection, 'incoming'); } } - if (event === 'contacts.update') { - this.logger.verbose('event contacts.update'); - const data = body; + // if (event === 'contacts.update') { + // this.logger.verbose('event contacts.update'); + // const data = body; - if (data.length) { - this.logger.verbose('contacts found'); - for (const item of data) { - const number = item.id.split('@')[0]; - const photo = item.profilePictureUrl || null; - this.logger.verbose('find contact in chatwoot'); - const find = await this.findContact(instance, number); + // if (data.length) { + // this.logger.verbose('contacts found'); + // for (const item of data) { + // const number = item.id.split('@')[0]; + // const photo = item.profilePictureUrl || null; + // this.logger.verbose('find contact in chatwoot'); + // const find = await this.findContact(instance, number); - if (find) { - this.logger.verbose('contact found'); + // if (find) { + // this.logger.verbose('contact found'); - this.logger.verbose('update contact in chatwoot'); - await this.updateContact(instance, find.id, { - avatar_url: photo, - }); - } - } - } - } + // this.logger.verbose('update contact in chatwoot'); + // await this.updateContact(instance, find.id, { + // avatar_url: photo, + // }); + // } + // } + // } + // } if (event === 'qrcode.updated') { this.logger.verbose('event qrcode.updated'); diff --git a/src/whatsapp/services/webhook.service.ts b/src/whatsapp/services/webhook.service.ts index dbc98d7c..2370e05b 100644 --- a/src/whatsapp/services/webhook.service.ts +++ b/src/whatsapp/services/webhook.service.ts @@ -18,9 +18,17 @@ export class WebhookService { public async find(instance: InstanceDto): Promise { try { this.logger.verbose('find webhook: ' + instance.instanceName); - return await this.waMonitor.waInstances[instance.instanceName].findWebhook(); + const result = await this.waMonitor.waInstances[ + instance.instanceName + ].findWebhook(); + + if (Object.keys(result).length === 0) { + throw new Error('Webhook not found'); + } + + return result; } catch (error) { - return { enabled: null, url: '' }; + return { enabled: false, url: '', events: [], webhook_by_events: false }; } } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 98ad8d17..0acca202 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -149,7 +149,7 @@ export class WAStartupService { private endSession = false; private logBaileys = this.configService.get('LOG').BAILEYS; - private chatwootService = new ChatwootService(waMonitor); + private chatwootService = new ChatwootService(waMonitor, this.configService); public set instanceName(name: string) { this.logger.verbose(`Initializing instance '${name}'`); @@ -343,6 +343,7 @@ export class WAStartupService { public async sendDataWebhook(event: Events, data: T, local = true) { const webhookGlobal = this.configService.get('WEBHOOK'); const webhookLocal = this.localWebhook.events; + const serverUrl = this.configService.get('SERVER').URL; const we = event.replace(/[\.-]/gm, '_').toUpperCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const instance = this.configService.get('AUTHENTICATION').INSTANCE; @@ -366,6 +367,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: this.localWebhook.url, + server_url: serverUrl, }); } @@ -377,6 +379,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: this.localWebhook.url, + server_url: serverUrl, }); } } catch (error) { @@ -390,6 +393,7 @@ export class WAStartupService { stack: error?.stack, name: error?.name, url: baseURL, + server_url: serverUrl, }); } } @@ -424,6 +428,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: localUrl, + server_url: serverUrl, }); } @@ -435,6 +440,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: localUrl, + server_url: serverUrl, }); } } catch (error) { @@ -448,6 +454,7 @@ export class WAStartupService { stack: error?.stack, name: error?.name, url: globalURL, + server_url: serverUrl, }); } } @@ -617,6 +624,17 @@ export class WAStartupService { β”‚ CONNECTED TO WHATSAPP β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜`.replace(/^ +/gm, ' '), ); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.CONNECTION_UPDATE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'open', + }, + ); + } } } @@ -1111,7 +1129,6 @@ export class WAStartupService { }, 'messages.update': async (args: WAMessageUpdate[], database: Database) => { - console.log(args); this.logger.verbose('Event received: messages.update'); const status: Record = { 0: 'ERROR', @@ -1299,6 +1316,7 @@ export class WAStartupService { const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); if (regexp.test(jid)) { const match = regexp.exec(jid); + if (match && (match[1] === '52' || match[1] === '54')) { const joker = Number.parseInt(match[3][0]); const ddd = Number.parseInt(match[2]); @@ -1335,30 +1353,27 @@ export class WAStartupService { private createJid(number: string): string { this.logger.verbose('Creating jid with number: ' + number); - const numberReplace = number.replace(/[^0-9]/g, ''); - - console.log('number', numberReplace); - if (numberReplace.includes('@g.us') || numberReplace.includes('@s.whatsapp.net')) { + if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); - return numberReplace; + return number; } - if (numberReplace.includes('@broadcast')) { + if (number.includes('@broadcast')) { this.logger.verbose('Number already contains @broadcast'); - return numberReplace; + return number; } - const formattedBRNumber = this.formatBRNumber(numberReplace); - if (formattedBRNumber !== numberReplace) { + const formattedBRNumber = this.formatBRNumber(number); + if (formattedBRNumber !== number) { this.logger.verbose( 'Jid created is whatsapp in format BR: ' + `${formattedBRNumber}@s.whatsapp.net`, ); return `${formattedBRNumber}@s.whatsapp.net`; } - const formattedMXARNumber = this.formatMXOrARNumber(numberReplace); + const formattedMXARNumber = this.formatMXOrARNumber(number); - if (formattedMXARNumber !== numberReplace) { + if (formattedMXARNumber !== number) { this.logger.verbose( 'Jid created is whatsapp in format MXAR: ' + `${formattedMXARNumber}@s.whatsapp.net`, @@ -1366,13 +1381,13 @@ export class WAStartupService { return `${formattedMXARNumber}@s.whatsapp.net`; } - if (numberReplace.includes('-')) { - this.logger.verbose('Jid created is group: ' + `${numberReplace}@g.us`); - return `${numberReplace}@g.us`; + if (number.includes('-')) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + return `${number}@g.us`; } - this.logger.verbose('Jid created is whatsapp: ' + `${numberReplace}@s.whatsapp.net`); - return `${numberReplace}@s.whatsapp.net`; + this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); + return `${number}@s.whatsapp.net`; } public async profilePicture(number: string) { @@ -1488,7 +1503,7 @@ export class WAStartupService { !message['poll'] && !message['sticker'] && !message['conversation'] && - !sender.includes('@broadcast') + sender !== 'status@broadcast' ) { if (!message['audio']) { this.logger.verbose('Sending message'); @@ -1507,18 +1522,18 @@ export class WAStartupService { } if (message['conversation']) { - console.log(message['conversation']); this.logger.verbose('Sending message'); return await this.client.sendMessage( sender, { text: message['conversation'], + mentions, } as unknown as AnyMessageContent, option as unknown as MiscMessageGenerationOptions, ); } - if (sender.includes('@broadcast')) { + if (sender === 'status@broadcast') { this.logger.verbose('Sending message'); return await this.client.sendMessage( sender, @@ -1865,6 +1880,8 @@ export class WAStartupService { this.logger.verbose('Sending media message'); const generate = await this.prepareMediaMessage(data.mediaMessage); + console.log('generate', generate); + return await this.sendMessageWithTyping( data.number, { ...generate.message }, diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index c57b9bf8..46f8ecd1 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -72,7 +72,7 @@ const webhookService = new WebhookService(waMonitor); export const webhookController = new WebhookController(webhookService); -const chatwootService = new ChatwootService(waMonitor); +const chatwootService = new ChatwootService(waMonitor, configService); export const chatwootController = new ChatwootController(chatwootService, configService);