mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-27 02:48:39 -06:00
Merge branch 'develop' into main
This commit is contained in:
commit
94af67f35a
3
.gitignore
vendored
3
.gitignore
vendored
@ -45,4 +45,5 @@ docker-compose.yaml
|
||||
/temp/*
|
||||
|
||||
.DS_Store
|
||||
*.DS_Store
|
||||
*.DS_Store
|
||||
.tool-versions
|
||||
|
15
CHANGELOG.md
15
CHANGELOG.md
@ -3,6 +3,10 @@
|
||||
### Feature
|
||||
|
||||
* 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
|
||||
|
||||
@ -16,6 +20,17 @@
|
||||
* 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
|
||||
* 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)
|
||||
|
||||
|
17
package.json
17
package.json
@ -46,11 +46,11 @@
|
||||
"@figuro/chatwoot-sdk": "^1.1.16",
|
||||
"@hapi/boom": "^10.0.1",
|
||||
"@sentry/node": "^7.59.2",
|
||||
"@whiskeysockets/baileys": "^6.5.0",
|
||||
"@whiskeysockets/baileys": "6.6.0",
|
||||
"amqplib": "^0.10.3",
|
||||
"aws-sdk": "^2.1499.0",
|
||||
"axios": "^1.3.5",
|
||||
"class-validator": "^0.13.2",
|
||||
"axios": "^1.6.5",
|
||||
"class-validator": "^0.14.1",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
@ -60,34 +60,39 @@
|
||||
"exiftool-vendored": "^22.0.0",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"fast-levenshtein": "^3.0.0",
|
||||
"hbs": "^4.2.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"i18next": "^23.7.19",
|
||||
"jimp": "^0.16.13",
|
||||
"join": "^3.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonschema": "^1.4.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"libphonenumber-js": "^1.10.39",
|
||||
"link-preview-js": "^3.0.4",
|
||||
"mongoose": "^6.10.5",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-mime-types": "^1.1.0",
|
||||
"node-windows": "^1.0.0-beta.8",
|
||||
"parse-bmfont-xml": "^1.1.4",
|
||||
"pino": "^8.11.0",
|
||||
"proxy-agent": "^6.3.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"redis": "^4.6.5",
|
||||
"sharp": "^0.30.7",
|
||||
"sharp": "^0.32.2",
|
||||
"socket.io": "^4.7.1",
|
||||
"socks-proxy-agent": "^8.0.1",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fast-levenshtein": "^0.0.4",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
|
@ -131,6 +131,8 @@ export type Auth = {
|
||||
|
||||
export type DelInstance = number | boolean;
|
||||
|
||||
export type Language = string | 'en';
|
||||
|
||||
export type GlobalWebhook = {
|
||||
URL: string;
|
||||
ENABLED: boolean;
|
||||
@ -151,6 +153,7 @@ export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook };
|
||||
export type ConfigSessionPhone = { CLIENT: string; NAME: string };
|
||||
export type QrCode = { LIMIT: number; COLOR: string };
|
||||
export type Typebot = { API_VERSION: string; KEEP_OPEN: boolean };
|
||||
export type ChatWoot = { MESSAGE_DELETE: boolean };
|
||||
export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal };
|
||||
export type Production = boolean;
|
||||
|
||||
@ -167,10 +170,12 @@ export interface Env {
|
||||
WEBSOCKET: Websocket;
|
||||
LOG: Log;
|
||||
DEL_INSTANCE: DelInstance;
|
||||
LANGUAGE: Language;
|
||||
WEBHOOK: Webhook;
|
||||
CONFIG_SESSION_PHONE: ConfigSessionPhone;
|
||||
QRCODE: QrCode;
|
||||
TYPEBOT: Typebot;
|
||||
CHATWOOT: ChatWoot;
|
||||
CACHE: CacheConf;
|
||||
AUTHENTICATION: Auth;
|
||||
PRODUCTION?: Production;
|
||||
@ -289,6 +294,7 @@ export class ConfigService {
|
||||
DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE)
|
||||
? process.env.DEL_INSTANCE === 'true'
|
||||
: Number.parseInt(process.env.DEL_INSTANCE) || false,
|
||||
LANGUAGE: process.env?.LANGUAGE || 'en',
|
||||
WEBHOOK: {
|
||||
GLOBAL: {
|
||||
URL: process.env?.WEBHOOK_GLOBAL_URL || '',
|
||||
@ -338,6 +344,9 @@ export class ConfigService {
|
||||
API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old',
|
||||
KEEP_OPEN: process.env.TYPEBOT_KEEP_OPEN === 'true',
|
||||
},
|
||||
CHATWOOT: {
|
||||
MESSAGE_DELETE: process.env.CHATWOOT_MESSAGE_DELETE === 'false',
|
||||
},
|
||||
CACHE: {
|
||||
REDIS: {
|
||||
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',
|
||||
|
@ -162,6 +162,10 @@ TYPEBOT:
|
||||
API_VERSION: 'old' # old | latest
|
||||
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:
|
||||
REDIS:
|
||||
@ -188,3 +192,6 @@ AUTHENTICATION:
|
||||
JWT:
|
||||
EXPIRIN_IN: 0 # seconds - 3600s === 1h | zero (0) - never expires
|
||||
SECRET: L=0YWt]b2w[WF>#>:&E`
|
||||
|
||||
|
||||
LANGUAGE: "pt-BR" # pt-BR, en
|
@ -940,7 +940,72 @@ paths:
|
||||
description: Successful response
|
||||
content:
|
||||
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}:
|
||||
post:
|
||||
tags:
|
||||
|
36
src/utils/i18n.ts
Normal file
36
src/utils/i18n.ts
Normal 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;
|
17
src/utils/makeProxyAgent.ts
Normal file
17
src/utils/makeProxyAgent.ts
Normal 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);
|
||||
}
|
6
src/utils/translations/en.json
Normal file
6
src/utils/translations/en.json
Normal 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."
|
||||
}
|
6
src/utils/translations/pt-BR.json
Normal file
6
src/utils/translations/pt-BR.json
Normal 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."
|
||||
}
|
@ -812,6 +812,16 @@ export const groupInviteSchema: JSONSchema7 = {
|
||||
...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 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import {
|
||||
AcceptGroupInvite,
|
||||
CreateGroupDto,
|
||||
GetParticipant,
|
||||
GroupDescriptionDto,
|
||||
@ -65,6 +66,11 @@ export class GroupController {
|
||||
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) {
|
||||
logger.verbose('requested revokeInviteCode from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(groupJid);
|
||||
|
@ -1,19 +1,25 @@
|
||||
import axios from 'axios';
|
||||
|
||||
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 { ProxyDto } from '../dto/proxy.dto';
|
||||
import { WAMonitoringService } from '../services/monitor.service';
|
||||
import { ProxyService } from '../services/proxy.service';
|
||||
|
||||
const logger = new Logger('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) {
|
||||
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) {
|
||||
logger.verbose('proxy disabled');
|
||||
data.proxy = null;
|
||||
@ -21,8 +27,7 @@ export class ProxyController {
|
||||
|
||||
if (data.proxy) {
|
||||
logger.verbose('proxy enabled');
|
||||
const { host, port, protocol, username, password } = data.proxy;
|
||||
const testProxy = await this.testProxy(host, port, protocol, username, password);
|
||||
const testProxy = await this.testProxy(data.proxy);
|
||||
if (!testProxy) {
|
||||
throw new BadRequestException('Invalid proxy');
|
||||
}
|
||||
@ -33,37 +38,30 @@ export class ProxyController {
|
||||
|
||||
public async findProxy(instance: InstanceDto) {
|
||||
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);
|
||||
}
|
||||
|
||||
private async testProxy(host: string, port: string, protocol: string, username?: string, password?: string) {
|
||||
private async testProxy(proxy: ProxyDto['proxy']) {
|
||||
logger.verbose('requested testProxy');
|
||||
try {
|
||||
let proxyConfig: any = {
|
||||
host: host,
|
||||
port: parseInt(port),
|
||||
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,
|
||||
const serverIp = await axios.get('https://icanhazip.com/');
|
||||
const response = await axios.get('https://icanhazip.com/', {
|
||||
httpsAgent: makeProxyAgent(proxy),
|
||||
});
|
||||
|
||||
logger.verbose('testProxy response: ' + response.data);
|
||||
return response.data !== serverIp.data;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys';
|
||||
|
||||
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 {
|
||||
|
@ -32,6 +32,10 @@ export class GroupInvite {
|
||||
inviteCode: string;
|
||||
}
|
||||
|
||||
export class AcceptGroupInvite {
|
||||
inviteCode: string;
|
||||
}
|
||||
|
||||
export class GroupSendInvite {
|
||||
groupJid: string;
|
||||
description: string;
|
||||
|
@ -26,7 +26,7 @@ export class MessageRaw {
|
||||
messageType?: string;
|
||||
messageTimestamp?: number | Long.Long;
|
||||
owner: string;
|
||||
source?: 'android' | 'web' | 'ios';
|
||||
source?: 'android' | 'web' | 'ios' | 'unknown' | 'desktop';
|
||||
source_id?: string;
|
||||
source_reply_id?: string;
|
||||
chatwoot?: ChatwootMessage;
|
||||
@ -45,7 +45,7 @@ const messageSchema = new Schema<MessageRaw>({
|
||||
participant: { type: String },
|
||||
messageType: { type: String },
|
||||
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 },
|
||||
owner: { type: String, required: true, minlength: 1 },
|
||||
chatwoot: {
|
||||
|
@ -2,6 +2,7 @@ import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import {
|
||||
AcceptGroupInviteSchema,
|
||||
createGroupSchema,
|
||||
getParticipantsSchema,
|
||||
groupInviteSchema,
|
||||
@ -16,6 +17,7 @@ import {
|
||||
} from '../../validate/validate.schema';
|
||||
import { RouterBroker } from '../abstract/abstract.router';
|
||||
import {
|
||||
AcceptGroupInvite,
|
||||
CreateGroupDto,
|
||||
GetParticipant,
|
||||
GroupDescriptionDto,
|
||||
@ -182,6 +184,22 @@ export class GroupRouter extends RouterBroker {
|
||||
|
||||
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) => {
|
||||
logger.verbose('request received in sendInvite');
|
||||
logger.verbose('request body: ');
|
||||
|
@ -7,8 +7,9 @@ import Jimp from 'jimp';
|
||||
import mimeTypes from 'mime-types';
|
||||
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 i18next from '../../utils/i18n';
|
||||
import { ICache } from '../abstract/abstract.cache';
|
||||
import { ChatwootDto } from '../dto/chatwoot.dto';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
@ -368,8 +369,9 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
let query: any;
|
||||
const isGroup = phoneNumber.includes('@g.us');
|
||||
|
||||
if (!phoneNumber.includes('@g.us')) {
|
||||
if (!isGroup) {
|
||||
this.logger.verbose('format phone number');
|
||||
query = `+${phoneNumber}`;
|
||||
} else {
|
||||
@ -378,25 +380,96 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
this.logger.verbose('find contact in chatwoot');
|
||||
const contact: any = await client.contacts.search({
|
||||
accountId: this.provider.account_id,
|
||||
q: query,
|
||||
});
|
||||
let contact: any;
|
||||
|
||||
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) {
|
||||
this.logger.warn('contact not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!phoneNumber.includes('@g.us')) {
|
||||
if (!isGroup) {
|
||||
this.logger.verbose('return contact');
|
||||
return contact.payload.find((contact) => contact.phone_number === query);
|
||||
return this.findContactInContactList(contact.payload, query);
|
||||
} else {
|
||||
this.logger.verbose('return group');
|
||||
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) {
|
||||
this.logger.verbose('create conversation to instance: ' + instance.instanceName);
|
||||
try {
|
||||
@ -1643,6 +1716,27 @@ export class ChatwootService {
|
||||
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') {
|
||||
this.logger.verbose('event messages.upsert');
|
||||
|
||||
@ -1924,31 +2018,34 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.logger.warn('message id not found');
|
||||
return;
|
||||
}
|
||||
if (!body?.key?.id) {
|
||||
this.logger.warn('message id not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.getMessageByKeyId(instance, body.key.id);
|
||||
if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) {
|
||||
this.logger.verbose('deleting message in repository. Message id: ' + body.key.id);
|
||||
this.repository.message.delete({
|
||||
where: {
|
||||
key: {
|
||||
id: body.key.id,
|
||||
const message = await this.getMessageByKeyId(instance, body.key.id);
|
||||
if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) {
|
||||
this.logger.verbose('deleting message in repository. Message id: ' + body.key.id);
|
||||
this.repository.message.delete({
|
||||
where: {
|
||||
key: {
|
||||
id: body.key.id,
|
||||
},
|
||||
owner: instance.instanceName,
|
||||
},
|
||||
owner: instance.instanceName,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id);
|
||||
return await client.messages.delete({
|
||||
accountId: this.provider.account_id,
|
||||
conversationId: message.chatwoot.conversationId,
|
||||
messageId: message.chatwoot.messageId,
|
||||
});
|
||||
this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id);
|
||||
return await client.messages.delete({
|
||||
accountId: this.provider.account_id,
|
||||
conversationId: message.chatwoot.conversationId,
|
||||
messageId: message.chatwoot.messageId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2024,7 +2121,8 @@ export class ChatwootService {
|
||||
this.logger.verbose('event qrcode.updated');
|
||||
if (body.statusCode === 500) {
|
||||
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');
|
||||
return await this.createBotMessage(instance, erroQRcode, 'incoming');
|
||||
@ -2040,9 +2138,9 @@ export class ChatwootService {
|
||||
writeFileSync(fileName, fileData, 'utf8');
|
||||
|
||||
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) {
|
||||
msgQrCode =
|
||||
|
@ -274,6 +274,7 @@ export class TypebotService {
|
||||
const types = {
|
||||
conversation: msg.conversation,
|
||||
extendedTextMessage: msg.extendedTextMessage?.text,
|
||||
responseRowId: msg.listResponseMessage?.singleSelectReply?.selectedRowId,
|
||||
};
|
||||
|
||||
this.logger.verbose('type message: ' + types);
|
||||
@ -412,7 +413,13 @@ export class TypebotService {
|
||||
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) {
|
||||
text += applyFormatting(child);
|
||||
}
|
||||
|
@ -2,8 +2,14 @@ import axios from 'axios';
|
||||
import { execSync } from 'child_process';
|
||||
import { isURL } from 'class-validator
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import levenshtein from 'fast-levenshtein';
|
||||
import fs, { existsSync, readFileSync } from 'fs';
|
||||
import Long from 'long';
|
||||
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 {
|
||||
@ -26,6 +32,7 @@ import { dbserver } from '../../libs/db.connect';
|
||||
import { RedisCache } from '../../libs/redis.client';
|
||||
import { getIO } from '../../libs/socket.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 { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db';
|
||||
import {
|
||||
@ -42,6 +49,7 @@ import {
|
||||
WhatsAppNumberDto,
|
||||
} from '../dto/chat.dto';
|
||||
import {
|
||||
AcceptGroupInvite,
|
||||
CreateGroupDto,
|
||||
GetParticipant,
|
||||
GroupDescriptionDto,
|
||||
@ -1173,24 +1181,21 @@ export class WAStartupService {
|
||||
this.logger.info('Proxy enabled: ' + this.localProxy.proxy);
|
||||
|
||||
if (this.localProxy.proxy.host.includes('proxyscrape')) {
|
||||
const response = await axios.get(this.localProxy.proxy.host);
|
||||
const text = response.data;
|
||||
const proxyUrls = text.split('\r\n');
|
||||
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
|
||||
const proxyUrl = 'http://' + proxyUrls[rand];
|
||||
options = {
|
||||
agent: new ProxyAgent(proxyUrl as any),
|
||||
};
|
||||
} else {
|
||||
let proxyUri =
|
||||
this.localProxy.proxy.protocol + '://' + this.localProxy.proxy.host + ':' + this.localProxy.proxy.port;
|
||||
|
||||
if (this.localProxy.proxy.username && this.localProxy.proxy.password) {
|
||||
proxyUri = `${this.localProxy.proxy.username}:${this.localProxy.proxy.password}@${proxyUri}`;
|
||||
try {
|
||||
const response = await axios.get(this.localProxy.proxy.host);
|
||||
const text = response.data;
|
||||
const proxyUrls = text.split('\r\n');
|
||||
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
|
||||
const proxyUrl = 'http://' + proxyUrls[rand];
|
||||
options = {
|
||||
agent: makeProxyAgent(proxyUrl),
|
||||
};
|
||||
} catch (error) {
|
||||
this.localProxy.enabled = false;
|
||||
}
|
||||
|
||||
} else {
|
||||
options = {
|
||||
agent: new ProxyAgent(proxyUri as any),
|
||||
agent: makeProxyAgent(this.localProxy.proxy),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1277,8 +1282,8 @@ export class WAStartupService {
|
||||
if (this.localProxy.enabled) {
|
||||
this.logger.verbose('Proxy enabled');
|
||||
options = {
|
||||
agent: new ProxyAgent(this.localProxy.proxy as any),
|
||||
fetchAgent: new ProxyAgent(this.localProxy.proxy as any),
|
||||
agent: makeProxyAgent(this.localProxy.proxy),
|
||||
fetchAgent: makeProxyAgent(this.localProxy.proxy),
|
||||
};
|
||||
}
|
||||
|
||||
@ -2147,7 +2152,15 @@ export class WAStartupService {
|
||||
const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift();
|
||||
|
||||
this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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');
|
||||
return await this.client.sendMessage(
|
||||
sender,
|
||||
@ -2917,31 +2930,84 @@ export class WAStartupService {
|
||||
public async whatsappNumber(data: WhatsAppNumberDto) {
|
||||
this.logger.verbose('Getting whatsapp number');
|
||||
|
||||
const onWhatsapp: OnWhatsAppDto[] = [];
|
||||
for await (const number of data.numbers) {
|
||||
let jid = this.createJid(number);
|
||||
const jids: {
|
||||
groups: { number: string; jid: string }[];
|
||||
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)) {
|
||||
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');
|
||||
|
||||
if (!group) throw new BadRequestException('Group not found');
|
||||
|
||||
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));
|
||||
if (!group) {
|
||||
new OnWhatsAppDto(jid, false, number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@ -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) {
|
||||
this.logger.verbose('Revoking invite code for group: ' + id.groupJid);
|
||||
try {
|
||||
|
@ -124,7 +124,7 @@ export const websocketController = new WebsocketController(websocketService);
|
||||
|
||||
const proxyService = new ProxyService(waMonitor);
|
||||
|
||||
export const proxyController = new ProxyController(proxyService);
|
||||
export const proxyController = new ProxyController(proxyService, waMonitor);
|
||||
|
||||
const chamaaiService = new ChamaaiService(waMonitor, configService);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user