Merge branch 'develop' into main

This commit is contained in:
Davidson Gomes 2024-02-02 14:07:47 -03:00 committed by GitHub
commit 94af67f35a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 498 additions and 109 deletions

3
.gitignore vendored
View File

@ -45,4 +45,5 @@ docker-compose.yaml
/temp/* /temp/*
.DS_Store .DS_Store
*.DS_Store *.DS_Store
.tool-versions

View File

@ -3,6 +3,10 @@
### Feature ### Feature
* Added update message endpoint * Added update message endpoint
* Add translate capabilities to QRMessages in CW
* Join in Group by Invite Code
* Read messages from whatsapp in chatwoot
* Add support to use use redis in cacheservice
### Fixed ### Fixed
@ -16,6 +20,17 @@
* When receiving a file from whatsapp, use the original filename in chatwoot if possible * When receiving a file from whatsapp, use the original filename in chatwoot if possible
* Remove message ids cache in chatwoot to use chatwoot's api itself * Remove message ids cache in chatwoot to use chatwoot's api itself
* Adjusts the quoted message, now has contextInfo in the message Raw * Adjusts the quoted message, now has contextInfo in the message Raw
* Collecting responses with text or numbers in Typebot
* Added sendList endpoint to swagger documentation
* Implemented a function to synchronize message deletions on WhatsApp, automatically reflecting in Chatwoot.
* Improvement on numbers validation
* Fix polls in message sending
* Sending status message
* Message 'connection successfully' spamming
* Invalidate the conversation cache if reopen_conversation is false and the conversation was resolved
* Fix looping when deleting a message in chatwoot
* When receiving a file from whatsapp, use the original filename in chatwoot if possible
* Correction in the sendList Function
# 1.6.1 (2023-12-22 11:43) # 1.6.1 (2023-12-22 11:43)

View File

@ -46,11 +46,11 @@
"@figuro/chatwoot-sdk": "^1.1.16", "@figuro/chatwoot-sdk": "^1.1.16",
"@hapi/boom": "^10.0.1", "@hapi/boom": "^10.0.1",
"@sentry/node": "^7.59.2", "@sentry/node": "^7.59.2",
"@whiskeysockets/baileys": "^6.5.0", "@whiskeysockets/baileys": "6.6.0",
"amqplib": "^0.10.3", "amqplib": "^0.10.3",
"aws-sdk": "^2.1499.0", "aws-sdk": "^2.1499.0",
"axios": "^1.3.5", "axios": "^1.6.5",
"class-validator": "^0.13.2", "class-validator": "^0.14.1",
"compression": "^1.7.4", "compression": "^1.7.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -60,34 +60,39 @@
"exiftool-vendored": "^22.0.0", "exiftool-vendored": "^22.0.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"fast-levenshtein": "^3.0.0",
"hbs": "^4.2.0", "hbs": "^4.2.0",
"https-proxy-agent": "^7.0.2",
"i18next": "^23.7.19",
"jimp": "^0.16.13", "jimp": "^0.16.13",
"join": "^3.0.0", "join": "^3.0.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.2",
"libphonenumber-js": "^1.10.39", "libphonenumber-js": "^1.10.39",
"link-preview-js": "^3.0.4", "link-preview-js": "^3.0.4",
"mongoose": "^6.10.5", "mongoose": "^6.10.5",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"node-mime-types": "^1.1.0", "node-mime-types": "^1.1.0",
"node-windows": "^1.0.0-beta.8", "node-windows": "^1.0.0-beta.8",
"parse-bmfont-xml": "^1.1.4",
"pino": "^8.11.0", "pino": "^8.11.0",
"proxy-agent": "^6.3.0",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"redis": "^4.6.5", "redis": "^4.6.5",
"sharp": "^0.30.7", "sharp": "^0.32.2",
"socket.io": "^4.7.1", "socket.io": "^4.7.1",
"socks-proxy-agent": "^8.0.1", "socks-proxy-agent": "^8.0.1",
"swagger-ui-express": "^5.0.0", "swagger-ui-express": "^5.0.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"xml2js": "^0.6.2",
"yamljs": "^0.3.0" "yamljs": "^0.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/cors": "^2.8.13", "@types/cors": "^2.8.13",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/fast-levenshtein": "^0.0.4",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",

View File

@ -131,6 +131,8 @@ export type Auth = {
export type DelInstance = number | boolean; export type DelInstance = number | boolean;
export type Language = string | 'en';
export type GlobalWebhook = { export type GlobalWebhook = {
URL: string; URL: string;
ENABLED: boolean; ENABLED: boolean;
@ -151,6 +153,7 @@ export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook };
export type ConfigSessionPhone = { CLIENT: string; NAME: string }; export type ConfigSessionPhone = { CLIENT: string; NAME: string };
export type QrCode = { LIMIT: number; COLOR: string }; export type QrCode = { LIMIT: number; COLOR: string };
export type Typebot = { API_VERSION: string; KEEP_OPEN: boolean }; export type Typebot = { API_VERSION: string; KEEP_OPEN: boolean };
export type ChatWoot = { MESSAGE_DELETE: boolean };
export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal }; export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal };
export type Production = boolean; export type Production = boolean;
@ -167,10 +170,12 @@ export interface Env {
WEBSOCKET: Websocket; WEBSOCKET: Websocket;
LOG: Log; LOG: Log;
DEL_INSTANCE: DelInstance; DEL_INSTANCE: DelInstance;
LANGUAGE: Language;
WEBHOOK: Webhook; WEBHOOK: Webhook;
CONFIG_SESSION_PHONE: ConfigSessionPhone; CONFIG_SESSION_PHONE: ConfigSessionPhone;
QRCODE: QrCode; QRCODE: QrCode;
TYPEBOT: Typebot; TYPEBOT: Typebot;
CHATWOOT: ChatWoot;
CACHE: CacheConf; CACHE: CacheConf;
AUTHENTICATION: Auth; AUTHENTICATION: Auth;
PRODUCTION?: Production; PRODUCTION?: Production;
@ -289,6 +294,7 @@ export class ConfigService {
DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE)
? process.env.DEL_INSTANCE === 'true' ? process.env.DEL_INSTANCE === 'true'
: Number.parseInt(process.env.DEL_INSTANCE) || false, : Number.parseInt(process.env.DEL_INSTANCE) || false,
LANGUAGE: process.env?.LANGUAGE || 'en',
WEBHOOK: { WEBHOOK: {
GLOBAL: { GLOBAL: {
URL: process.env?.WEBHOOK_GLOBAL_URL || '', URL: process.env?.WEBHOOK_GLOBAL_URL || '',
@ -338,6 +344,9 @@ export class ConfigService {
API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old', API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old',
KEEP_OPEN: process.env.TYPEBOT_KEEP_OPEN === 'true', KEEP_OPEN: process.env.TYPEBOT_KEEP_OPEN === 'true',
}, },
CHATWOOT: {
MESSAGE_DELETE: process.env.CHATWOOT_MESSAGE_DELETE === 'false',
},
CACHE: { CACHE: {
REDIS: { REDIS: {
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true', ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',

View File

@ -162,6 +162,10 @@ TYPEBOT:
API_VERSION: 'old' # old | latest API_VERSION: 'old' # old | latest
KEEP_OPEN: false KEEP_OPEN: false
# If you leave this option as false, when deleting the message for everyone on WhatsApp, it will not be deleted on Chatwoot.
CHATWOOT:
MESSAGE_DELETE: true # false | true
# Cache to optimize application performance # Cache to optimize application performance
CACHE: CACHE:
REDIS: REDIS:
@ -188,3 +192,6 @@ AUTHENTICATION:
JWT: JWT:
EXPIRIN_IN: 0 # seconds - 3600s === 1h | zero (0) - never expires EXPIRIN_IN: 0 # seconds - 3600s === 1h | zero (0) - never expires
SECRET: L=0YWt]b2w[WF>#>:&E` SECRET: L=0YWt]b2w[WF>#>:&E`
LANGUAGE: "pt-BR" # pt-BR, en

View File

@ -940,7 +940,72 @@ paths:
description: Successful response description: Successful response
content: content:
application/json: {} application/json: {}
/message/sendList/{instanceName}:
post:
tags:
- Send Message Controller
summary: Send a list to a specified instance.
description: This endpoint allows users to send a list to a chat.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
number:
type: string
options:
type: object
properties:
delay:
type: integer
presence:
type: string
listMessage:
type: object
properties:
title:
type: string
description:
type: string
footerText:
type: string
nullable: true
buttonText:
type: string
sections:
type: array
items:
type: object
properties:
title:
type: string
rows:
type: array
items:
type: object
properties:
title:
type: string
description:
type: string
rowId:
type: string
parameters:
- name: instanceName
in: path
required: true
schema:
type: string
description: The name of the instance to which the poll should be sent.
example: "evolution"
responses:
"200":
description: Successful response
content:
application/json: {}
/chat/whatsappNumbers/{instanceName}: /chat/whatsappNumbers/{instanceName}:
post: post:
tags: tags:

36
src/utils/i18n.ts Normal file
View File

@ -0,0 +1,36 @@
import fs from 'fs';
import i18next from 'i18next';
import path from 'path';
import { ConfigService, Language } from '../config/env.config';
// export class i18n {
// constructor(private readonly configService: ConfigService) {
const languages = ['en', 'pt-BR'];
const translationsPath = path.join(__dirname, 'translations');
const configService: ConfigService = new ConfigService();
const resources: any = {};
languages.forEach((language) => {
const languagePath = path.join(translationsPath, `${language}.json`);
if (fs.existsSync(languagePath)) {
resources[language] = {
translation: require(languagePath),
};
}
});
i18next.init({
resources,
fallbackLng: 'en',
lng: configService.get<Language>('LANGUAGE'),
debug: false,
interpolation: {
escapeValue: false,
},
});
// }
// }
export default i18next;

View File

@ -0,0 +1,17 @@
import { HttpsProxyAgent } from 'https-proxy-agent';
import { wa } from '../whatsapp/types/wa.types';
export function makeProxyAgent(proxy: wa.Proxy | string) {
if (typeof proxy === 'string') {
return new HttpsProxyAgent(proxy);
}
const { host, password, port, protocol, username } = proxy;
let proxyUrl = `${protocol}://${host}:${port}`;
if (username && password) {
proxyUrl = `${protocol}://${username}:${password}@${host}:${port}`;
}
return new HttpsProxyAgent(proxyUrl);
}

View File

@ -0,0 +1,6 @@
{
"qrgeneratedsuccesfully": "QRCode successfully generated!",
"scanqr": "Scan this QR code within the next 40 seconds.",
"qrlimitreached": "QRCode generation limit reached, to generate a new QRCode, send the 'init' message again.",
"numbernotinwhatsapp": "The message was not sent as the contact is not a valid Whatsapp number."
}

View File

@ -0,0 +1,6 @@
{
"qrgeneratedsuccesfully": "QRCode gerado com sucesso!",
"scanqr": "Escanei o QRCode com o Whatsapp nos próximos 40 segundos.",
"qrlimitreached": "Limite de geração de QRCode atingido! Para gerar um novo QRCode, envie o texto 'init' nesta conversa.",
"numbernotinwhatsapp": "A mensagem não foi enviada, pois o contato não é um número válido do Whatsapp."
}

View File

@ -812,6 +812,16 @@ export const groupInviteSchema: JSONSchema7 = {
...isNotEmpty('inviteCode'), ...isNotEmpty('inviteCode'),
}; };
export const AcceptGroupInviteSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
inviteCode: { type: 'string', pattern: '^[a-zA-Z0-9]{22}$' },
},
required: ['inviteCode'],
...isNotEmpty('inviteCode'),
};
export const updateParticipantsSchema: JSONSchema7 = { export const updateParticipantsSchema: JSONSchema7 = {
$id: v4(), $id: v4(),
type: 'object', type: 'object',

View File

@ -1,5 +1,6 @@
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { import {
AcceptGroupInvite,
CreateGroupDto, CreateGroupDto,
GetParticipant, GetParticipant,
GroupDescriptionDto, GroupDescriptionDto,
@ -65,6 +66,11 @@ export class GroupController {
return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data); return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data);
} }
public async acceptInviteCode(instance: InstanceDto, inviteCode: AcceptGroupInvite) {
logger.verbose('requested acceptInviteCode from ' + instance.instanceName + ' instance');
return await this.waMonitor.waInstances[instance.instanceName].acceptInviteCode(inviteCode);
}
public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) { public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) {
logger.verbose('requested revokeInviteCode from ' + instance.instanceName + ' instance'); logger.verbose('requested revokeInviteCode from ' + instance.instanceName + ' instance');
return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(groupJid); return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(groupJid);

View File

@ -1,19 +1,25 @@
import axios from 'axios'; import axios from 'axios';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { BadRequestException } from '../../exceptions'; import { BadRequestException, NotFoundException } from '../../exceptions';
import { makeProxyAgent } from '../../utils/makeProxyAgent';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
import { ProxyDto } from '../dto/proxy.dto'; import { ProxyDto } from '../dto/proxy.dto';
import { WAMonitoringService } from '../services/monitor.service';
import { ProxyService } from '../services/proxy.service'; import { ProxyService } from '../services/proxy.service';
const logger = new Logger('ProxyController'); const logger = new Logger('ProxyController');
export class ProxyController { export class ProxyController {
constructor(private readonly proxyService: ProxyService) {} constructor(private readonly proxyService: ProxyService, private readonly waMonitor: WAMonitoringService) {}
public async createProxy(instance: InstanceDto, data: ProxyDto) { public async createProxy(instance: InstanceDto, data: ProxyDto) {
logger.verbose('requested createProxy from ' + instance.instanceName + ' instance'); logger.verbose('requested createProxy from ' + instance.instanceName + ' instance');
if (!this.waMonitor.waInstances[instance.instanceName]) {
throw new NotFoundException(`The "${instance.instanceName}" instance does not exist`);
}
if (!data.enabled) { if (!data.enabled) {
logger.verbose('proxy disabled'); logger.verbose('proxy disabled');
data.proxy = null; data.proxy = null;
@ -21,8 +27,7 @@ export class ProxyController {
if (data.proxy) { if (data.proxy) {
logger.verbose('proxy enabled'); logger.verbose('proxy enabled');
const { host, port, protocol, username, password } = data.proxy; const testProxy = await this.testProxy(data.proxy);
const testProxy = await this.testProxy(host, port, protocol, username, password);
if (!testProxy) { if (!testProxy) {
throw new BadRequestException('Invalid proxy'); throw new BadRequestException('Invalid proxy');
} }
@ -33,37 +38,30 @@ export class ProxyController {
public async findProxy(instance: InstanceDto) { public async findProxy(instance: InstanceDto) {
logger.verbose('requested findProxy from ' + instance.instanceName + ' instance'); logger.verbose('requested findProxy from ' + instance.instanceName + ' instance');
if (!this.waMonitor.waInstances[instance.instanceName]) {
throw new NotFoundException(`The "${instance.instanceName}" instance does not exist`);
}
return this.proxyService.find(instance); return this.proxyService.find(instance);
} }
private async testProxy(host: string, port: string, protocol: string, username?: string, password?: string) { private async testProxy(proxy: ProxyDto['proxy']) {
logger.verbose('requested testProxy'); logger.verbose('requested testProxy');
try { try {
let proxyConfig: any = { const serverIp = await axios.get('https://icanhazip.com/');
host: host, const response = await axios.get('https://icanhazip.com/', {
port: parseInt(port), httpsAgent: makeProxyAgent(proxy),
protocol: protocol,
};
if (username && password) {
proxyConfig = {
...proxyConfig,
auth: {
username: username,
password: password,
},
};
}
const serverIp = await axios.get('http://meuip.com/api/meuip.php');
const response = await axios.get('http://meuip.com/api/meuip.php', {
proxy: proxyConfig,
}); });
logger.verbose('testProxy response: ' + response.data); logger.verbose('testProxy response: ' + response.data);
return response.data !== serverIp.data; return response.data !== serverIp.data;
} catch (error) { } catch (error) {
logger.error('testProxy error: ' + error); let errorMessage = error;
if (axios.isAxiosError(error) && error.response.data) {
errorMessage = error.response.data;
}
logger.error('testProxy error: ' + errorMessage);
return false; return false;
} }
} }

View File

@ -1,7 +1,12 @@
import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys'; import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys';
export class OnWhatsAppDto { export class OnWhatsAppDto {
constructor(public readonly jid: string, public readonly exists: boolean, public readonly name?: string) {} constructor(
public readonly jid: string,
public readonly exists: boolean,
public readonly number: string,
public readonly name?: string,
) {}
} }
export class getBase64FromMediaMessageDto { export class getBase64FromMediaMessageDto {

View File

@ -32,6 +32,10 @@ export class GroupInvite {
inviteCode: string; inviteCode: string;
} }
export class AcceptGroupInvite {
inviteCode: string;
}
export class GroupSendInvite { export class GroupSendInvite {
groupJid: string; groupJid: string;
description: string; description: string;

View File

@ -26,7 +26,7 @@ export class MessageRaw {
messageType?: string; messageType?: string;
messageTimestamp?: number | Long.Long; messageTimestamp?: number | Long.Long;
owner: string; owner: string;
source?: 'android' | 'web' | 'ios'; source?: 'android' | 'web' | 'ios' | 'unknown' | 'desktop';
source_id?: string; source_id?: string;
source_reply_id?: string; source_reply_id?: string;
chatwoot?: ChatwootMessage; chatwoot?: ChatwootMessage;
@ -45,7 +45,7 @@ const messageSchema = new Schema<MessageRaw>({
participant: { type: String }, participant: { type: String },
messageType: { type: String }, messageType: { type: String },
message: { type: Object }, message: { type: Object },
source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] }, source: { type: String, minlength: 3, enum: ['android', 'web', 'ios', 'unknown', 'desktop'] },
messageTimestamp: { type: Number, required: true }, messageTimestamp: { type: Number, required: true },
owner: { type: String, required: true, minlength: 1 }, owner: { type: String, required: true, minlength: 1 },
chatwoot: { chatwoot: {

View File

@ -2,6 +2,7 @@ import { RequestHandler, Router } from 'express';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { import {
AcceptGroupInviteSchema,
createGroupSchema, createGroupSchema,
getParticipantsSchema, getParticipantsSchema,
groupInviteSchema, groupInviteSchema,
@ -16,6 +17,7 @@ import {
} from '../../validate/validate.schema'; } from '../../validate/validate.schema';
import { RouterBroker } from '../abstract/abstract.router'; import { RouterBroker } from '../abstract/abstract.router';
import { import {
AcceptGroupInvite,
CreateGroupDto, CreateGroupDto,
GetParticipant, GetParticipant,
GroupDescriptionDto, GroupDescriptionDto,
@ -182,6 +184,22 @@ export class GroupRouter extends RouterBroker {
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
}) })
.get(this.routerPath('acceptInviteCode'), ...guards, async (req, res) => {
logger.verbose('request received in acceptInviteCode');
logger.verbose('request body: ');
logger.verbose(req.body);
logger.verbose('request query: ');
logger.verbose(req.query);
const response = await this.inviteCodeValidate<AcceptGroupInvite>({
request: req,
schema: AcceptGroupInviteSchema,
ClassRef: AcceptGroupInvite,
execute: (instance, data) => groupController.acceptInviteCode(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('sendInvite'), ...guards, async (req, res) => { .post(this.routerPath('sendInvite'), ...guards, async (req, res) => {
logger.verbose('request received in sendInvite'); logger.verbose('request received in sendInvite');
logger.verbose('request body: '); logger.verbose('request body: ');

View File

@ -7,8 +7,9 @@ import Jimp from 'jimp';
import mimeTypes from 'mime-types'; import mimeTypes from 'mime-types';
import path from 'path'; import path from 'path';
import { ConfigService, HttpServer, WABussiness } from '../../config/env.config'; import { ConfigService, HttpServer, WABussiness, ChatWoot, ConfigService, HttpServer } from '../../config/env.config';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import i18next from '../../utils/i18n';
import { ICache } from '../abstract/abstract.cache'; import { ICache } from '../abstract/abstract.cache';
import { ChatwootDto } from '../dto/chatwoot.dto'; import { ChatwootDto } from '../dto/chatwoot.dto';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
@ -368,8 +369,9 @@ export class ChatwootService {
} }
let query: any; let query: any;
const isGroup = phoneNumber.includes('@g.us');
if (!phoneNumber.includes('@g.us')) { if (!isGroup) {
this.logger.verbose('format phone number'); this.logger.verbose('format phone number');
query = `+${phoneNumber}`; query = `+${phoneNumber}`;
} else { } else {
@ -378,25 +380,96 @@ export class ChatwootService {
} }
this.logger.verbose('find contact in chatwoot'); this.logger.verbose('find contact in chatwoot');
const contact: any = await client.contacts.search({ let contact: any;
accountId: this.provider.account_id,
q: query, if (isGroup) {
}); contact = await client.contacts.search({
accountId: this.provider.account_id,
q: query,
});
} else {
// contact = await client.contacts.filter({
// accountId: this.provider.account_id,
// payload: this.getFilterPayload(query),
// });
// hotfix for: https://github.com/EvolutionAPI/evolution-api/pull/382. waiting fix: https://github.com/figurolatam/chatwoot-sdk/pull/7
contact = await chatwootRequest(this.getClientCwConfig(), {
method: 'POST',
url: `/api/v1/accounts/${this.provider.account_id}/contacts/filter`,
body: {
payload: this.getFilterPayload(query),
},
});
}
if (!contact) { if (!contact) {
this.logger.warn('contact not found'); this.logger.warn('contact not found');
return null; return null;
} }
if (!phoneNumber.includes('@g.us')) { if (!isGroup) {
this.logger.verbose('return contact'); this.logger.verbose('return contact');
return contact.payload.find((contact) => contact.phone_number === query); return this.findContactInContactList(contact.payload, query);
} else { } else {
this.logger.verbose('return group'); this.logger.verbose('return group');
return contact.payload.find((contact) => contact.identifier === query); return contact.payload.find((contact) => contact.identifier === query);
} }
} }
private findContactInContactList(contacts: any[], query: string) {
const phoneNumbers = this.getNumbers(query);
const searchableFields = this.getSearchableFields();
for (const contact of contacts) {
for (const field of searchableFields) {
if (contact[field] && phoneNumbers.includes(contact[field])) {
return contact;
}
}
}
return null;
}
private getNumbers(query: string) {
const numbers = [];
numbers.push(query);
if (query.startsWith('+55') && query.length === 14) {
const withoutNine = query.slice(0, 5) + query.slice(6);
numbers.push(withoutNine);
} else if (query.startsWith('+55') && query.length === 13) {
const withNine = query.slice(0, 5) + '9' + query.slice(5);
numbers.push(withNine);
}
return numbers;
}
private getSearchableFields() {
return ['phone_number'];
}
private getFilterPayload(query: string) {
const filterPayload = [];
const numbers = this.getNumbers(query);
const fieldsToSearch = this.getSearchableFields();
fieldsToSearch.forEach((field, index1) => {
numbers.forEach((number, index2) => {
const queryOperator = fieldsToSearch.length - 1 === index1 && numbers.length - 1 === index2 ? null : 'OR';
filterPayload.push({
attribute_key: field,
filter_operator: 'equal_to',
values: [number.replace('+', '')],
query_operator: queryOperator,
});
});
});
return filterPayload;
}
public async createConversation(instance: InstanceDto, body: any) { public async createConversation(instance: InstanceDto, body: any) {
this.logger.verbose('create conversation to instance: ' + instance.instanceName); this.logger.verbose('create conversation to instance: ' + instance.instanceName);
try { try {
@ -1643,6 +1716,27 @@ export class ChatwootService {
return null; return null;
} }
if (event === 'contact.is_not_in_wpp') {
const getConversation = await this.createConversation(instance, body);
if (!getConversation) {
this.logger.warn('conversation not found');
return;
}
client.messages.create({
accountId: this.provider.account_id,
conversationId: getConversation,
data: {
content: `🚨 ${i18next.t('numbernotinwhatsapp')}`,
message_type: 'outgoing',
private: true,
},
});
return;
}
if (event === 'messages.upsert' || event === 'send.message') { if (event === 'messages.upsert' || event === 'send.message') {
this.logger.verbose('event messages.upsert'); this.logger.verbose('event messages.upsert');
@ -1924,31 +2018,34 @@ export class ChatwootService {
} }
if (event === Events.MESSAGES_DELETE) { if (event === Events.MESSAGES_DELETE) {
this.logger.verbose('deleting message from instance: ' + instance.instanceName); const chatwootDelete = this.configService.get<ChatWoot>('CHATWOOT').MESSAGE_DELETE;
if (chatwootDelete === true) {
this.logger.verbose('deleting message from instance: ' + instance.instanceName);
if (!body?.key?.id) { if (!body?.key?.id) {
this.logger.warn('message id not found'); this.logger.warn('message id not found');
return; return;
} }
const message = await this.getMessageByKeyId(instance, body.key.id); const message = await this.getMessageByKeyId(instance, body.key.id);
if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) { if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) {
this.logger.verbose('deleting message in repository. Message id: ' + body.key.id); this.logger.verbose('deleting message in repository. Message id: ' + body.key.id);
this.repository.message.delete({ this.repository.message.delete({
where: { where: {
key: { key: {
id: body.key.id, id: body.key.id,
},
owner: instance.instanceName,
}, },
owner: instance.instanceName, });
},
});
this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id); this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id);
return await client.messages.delete({ return await client.messages.delete({
accountId: this.provider.account_id, accountId: this.provider.account_id,
conversationId: message.chatwoot.conversationId, conversationId: message.chatwoot.conversationId,
messageId: message.chatwoot.messageId, messageId: message.chatwoot.messageId,
}); });
}
} }
} }
@ -2024,7 +2121,8 @@ export class ChatwootService {
this.logger.verbose('event qrcode.updated'); this.logger.verbose('event qrcode.updated');
if (body.statusCode === 500) { if (body.statusCode === 500) {
this.logger.verbose('qrcode error'); this.logger.verbose('qrcode error');
const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the 'init' message again.`;
const erroQRcode = `🚨 ${i18next.t('qrlimitreached')}`;
this.logger.verbose('send message to chatwoot'); this.logger.verbose('send message to chatwoot');
return await this.createBotMessage(instance, erroQRcode, 'incoming'); return await this.createBotMessage(instance, erroQRcode, 'incoming');
@ -2040,9 +2138,9 @@ export class ChatwootService {
writeFileSync(fileName, fileData, 'utf8'); writeFileSync(fileName, fileData, 'utf8');
this.logger.verbose('send qrcode to chatwoot'); this.logger.verbose('send qrcode to chatwoot');
await this.createBotQr(instance, 'QRCode successfully generated!', 'incoming', fileName); await this.createBotQr(instance, i18next.t('qrgeneratedsuccesfully'), 'incoming', fileName);
let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; let msgQrCode = `⚡️${i18next.t('qrgeneratedsuccesfully')}\n\n${i18next.t('scanqr')}`;
if (body?.qrcode?.pairingCode) { if (body?.qrcode?.pairingCode) {
msgQrCode = msgQrCode =

View File

@ -274,6 +274,7 @@ export class TypebotService {
const types = { const types = {
conversation: msg.conversation, conversation: msg.conversation,
extendedTextMessage: msg.extendedTextMessage?.text, extendedTextMessage: msg.extendedTextMessage?.text,
responseRowId: msg.listResponseMessage?.singleSelectReply?.selectedRowId,
}; };
this.logger.verbose('type message: ' + types); this.logger.verbose('type message: ' + types);
@ -412,7 +413,13 @@ export class TypebotService {
text += element.text; text += element.text;
} }
if (element.type === 'p' || element.type === 'inline-variable' || element.type === 'a') { if (
element.children &&
(element.type === 'p' ||
element.type === 'a' ||
element.type === 'inline-variable' ||
element.type === 'variable')
) {
for (const child of element.children) { for (const child of element.children) {
text += applyFormatting(child); text += applyFormatting(child);
} }

View File

@ -2,8 +2,14 @@ import axios from 'axios';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { isURL } from 'class-validator import { isURL } from 'class-validator
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import levenshtein from 'fast-levenshtein';
import fs, { existsSync, readFileSync } from 'fs';
import Long from 'long'; import Long from 'long';
import { join } from 'path'; import { join } from 'path';
import P from 'pino';
import qrcode, { QRCodeToDataURLOptions } from 'qrcode';
import qrcodeTerminal from 'qrcode-terminal';
import sharp from 'sharp';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { import {
@ -26,6 +32,7 @@ import { dbserver } from '../../libs/db.connect';
import { RedisCache } from '../../libs/redis.client'; import { RedisCache } from '../../libs/redis.client';
import { getIO } from '../../libs/socket.server'; import { getIO } from '../../libs/socket.server';
import { getSQS, removeQueues as removeQueuesSQS } from '../../libs/sqs.server'; import { getSQS, removeQueues as removeQueuesSQS } from '../../libs/sqs.server';
import { makeProxyAgent } from '../../utils/makeProxyAgent';
import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db';
import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db';
import { import {
@ -42,6 +49,7 @@ import {
WhatsAppNumberDto, WhatsAppNumberDto,
} from '../dto/chat.dto'; } from '../dto/chat.dto';
import { import {
AcceptGroupInvite,
CreateGroupDto, CreateGroupDto,
GetParticipant, GetParticipant,
GroupDescriptionDto, GroupDescriptionDto,
@ -1173,24 +1181,21 @@ export class WAStartupService {
this.logger.info('Proxy enabled: ' + this.localProxy.proxy); this.logger.info('Proxy enabled: ' + this.localProxy.proxy);
if (this.localProxy.proxy.host.includes('proxyscrape')) { if (this.localProxy.proxy.host.includes('proxyscrape')) {
const response = await axios.get(this.localProxy.proxy.host); try {
const text = response.data; const response = await axios.get(this.localProxy.proxy.host);
const proxyUrls = text.split('\r\n'); const text = response.data;
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); const proxyUrls = text.split('\r\n');
const proxyUrl = 'http://' + proxyUrls[rand]; const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
options = { const proxyUrl = 'http://' + proxyUrls[rand];
agent: new ProxyAgent(proxyUrl as any), options = {
}; agent: makeProxyAgent(proxyUrl),
} else { };
let proxyUri = } catch (error) {
this.localProxy.proxy.protocol + '://' + this.localProxy.proxy.host + ':' + this.localProxy.proxy.port; this.localProxy.enabled = false;
if (this.localProxy.proxy.username && this.localProxy.proxy.password) {
proxyUri = `${this.localProxy.proxy.username}:${this.localProxy.proxy.password}@${proxyUri}`;
} }
} else {
options = { options = {
agent: new ProxyAgent(proxyUri as any), agent: makeProxyAgent(this.localProxy.proxy),
}; };
} }
} }
@ -1277,8 +1282,8 @@ export class WAStartupService {
if (this.localProxy.enabled) { if (this.localProxy.enabled) {
this.logger.verbose('Proxy enabled'); this.logger.verbose('Proxy enabled');
options = { options = {
agent: new ProxyAgent(this.localProxy.proxy as any), agent: makeProxyAgent(this.localProxy.proxy),
fetchAgent: new ProxyAgent(this.localProxy.proxy as any), fetchAgent: makeProxyAgent(this.localProxy.proxy),
}; };
} }
@ -2147,7 +2152,15 @@ export class WAStartupService {
const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift();
this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`); this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`);
if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) {
if (this.localChatwoot.enabled) {
const body = {
key: { remoteJid: isWA.jid },
};
this.chatwootService.eventWhatsapp('contact.is_not_in_wpp', { instanceName: this.instance.name }, body);
}
throw new BadRequestException(isWA); throw new BadRequestException(isWA);
} }
@ -2260,7 +2273,7 @@ export class WAStartupService {
); );
} }
if (!message['audio'] && sender != 'status@broadcast') { if (!message['audio'] && !message['poll'] && sender != 'status@broadcast') {
this.logger.verbose('Sending message'); this.logger.verbose('Sending message');
return await this.client.sendMessage( return await this.client.sendMessage(
sender, sender,
@ -2917,31 +2930,84 @@ export class WAStartupService {
public async whatsappNumber(data: WhatsAppNumberDto) { public async whatsappNumber(data: WhatsAppNumberDto) {
this.logger.verbose('Getting whatsapp number'); this.logger.verbose('Getting whatsapp number');
const onWhatsapp: OnWhatsAppDto[] = []; const jids: {
for await (const number of data.numbers) { groups: { number: string; jid: string }[];
let jid = this.createJid(number); broadcast: { number: string; jid: string }[];
users: { number: string; jid: string; name?: string }[];
} = {
groups: [],
broadcast: [],
users: [],
};
data.numbers.forEach((number) => {
const jid = this.createJid(number);
if (isJidGroup(jid)) { if (isJidGroup(jid)) {
jids.groups.push({ number, jid });
} else if (jid === 'status@broadcast') {
jids.broadcast.push({ number, jid });
} else {
jids.users.push({ number, jid });
}
});
const onWhatsapp: OnWhatsAppDto[] = [];
// BROADCAST
onWhatsapp.push(...jids.broadcast.map(({ jid, number }) => new OnWhatsAppDto(jid, false, number)));
// GROUPS
const groups = await Promise.all(
jids.groups.map(async ({ jid, number }) => {
const group = await this.findGroup({ groupJid: jid }, 'inner'); const group = await this.findGroup({ groupJid: jid }, 'inner');
if (!group) throw new BadRequestException('Group not found'); if (!group) {
new OnWhatsAppDto(jid, false, number);
onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject));
} else if (jid === 'status@broadcast') {
onWhatsapp.push(new OnWhatsAppDto(jid, false));
} else {
jid = !jid.startsWith('+') ? `+${jid}` : jid;
const verify = await this.client.onWhatsApp(jid);
const result = verify[0];
if (!result) {
onWhatsapp.push(new OnWhatsAppDto(jid, false));
} else {
onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists));
} }
}
} return new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject);
}),
);
onWhatsapp.push(...groups);
// USERS
const verify = await this.client.onWhatsApp(
...jids.users.map(({ jid }) => (!jid.startsWith('+') ? `+${jid}` : jid)),
);
const users: OnWhatsAppDto[] = await Promise.all(
jids.users.map(async (user) => {
const MAX_SIMILARITY_THRESHOLD = 0.01;
const isBrWithDigit = user.jid.startsWith('55') && user.jid.slice(4, 5) === '9' && user.jid.length === 28;
const jid = isBrWithDigit ? user.jid.slice(0, 4) + user.jid.slice(5) : user.jid;
const query: ContactQuery = {
where: {
owner: this.instance.name,
id: user.jid.startsWith('+') ? user.jid.substring(1) : user.jid,
},
};
const contacts: ContactRaw[] = await this.repository.contact.find(query);
let firstContactFound;
if (contacts.length > 0) {
firstContactFound = contacts[0].pushName;
}
const numberVerified = verify.find((v) => {
const mainJidSimilarity = levenshtein.get(user.jid, v.jid) / Math.max(user.jid.length, v.jid.length);
const jidSimilarity = levenshtein.get(jid, v.jid) / Math.max(jid.length, v.jid.length);
return mainJidSimilarity <= MAX_SIMILARITY_THRESHOLD || jidSimilarity <= MAX_SIMILARITY_THRESHOLD;
});
return {
exists: !!numberVerified?.exists,
jid: numberVerified?.jid || user.jid,
name: firstContactFound,
number: user.number,
};
}),
);
onWhatsapp.push(...users);
return onWhatsapp; return onWhatsapp;
} }
@ -3533,6 +3599,16 @@ export class WAStartupService {
} }
} }
public async acceptInviteCode(id: AcceptGroupInvite) {
this.logger.verbose('Joining the group by invitation code: ' + id.inviteCode);
try {
const groupJid = await this.client.groupAcceptInvite(id.inviteCode);
return { accepted: true, groupJid: groupJid };
} catch (error) {
throw new NotFoundException('Accept invite error', error.toString());
}
}
public async revokeInviteCode(id: GroupJid) { public async revokeInviteCode(id: GroupJid) {
this.logger.verbose('Revoking invite code for group: ' + id.groupJid); this.logger.verbose('Revoking invite code for group: ' + id.groupJid);
try { try {

View File

@ -124,7 +124,7 @@ export const websocketController = new WebsocketController(websocketService);
const proxyService = new ProxyService(waMonitor); const proxyService = new ProxyService(waMonitor);
export const proxyController = new ProxyController(proxyService); export const proxyController = new ProxyController(proxyService, waMonitor);
const chamaaiService = new ChamaaiService(waMonitor, configService); const chamaaiService = new ChamaaiService(waMonitor, configService);

0
start.sh Executable file → Normal file
View File