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/*
.DS_Store
*.DS_Store
*.DS_Store
.tool-versions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

0
start.sh Executable file → Normal file
View File