Compare commits

...

30 Commits
1.8.2 ... v1.8

Author SHA1 Message Date
Davidson Gomes
5d91876974 feat: Restart WebSocket connection on Wavoip token update
- Added logic to restart the WebSocket connection in `ChannelStartupService` when a valid `wavoipToken` is set, ensuring the client reconnects with updated settings.
2025-05-29 19:43:54 -03:00
Davidson Gomes
5043ce8405 feat: Integrate Wavoip for voice call functionality
- Added Wavoip integration to support voice calls within the application.
- Introduced `wavoipToken` in various DTOs and models to manage authentication.
- Updated `ChannelStartupService` to handle Wavoip settings and events.
- Enhanced `BaileysStartupService` to utilize Wavoip for call signaling.
- Updated CHANGELOG.md to version 1.8.7.
2025-05-29 19:20:09 -03:00
Davidson Gomes
8e65526ce9 Merge branch 'v1.8' of github.com:EvolutionAPI/evolution-api into v1.8 2025-05-22 16:53:28 -03:00
Davidson Gomes
cae016f40a fix: Prevent duplicate contact processing in ChannelStartupService
- Added a Set to track seen contact IDs, ensuring that each contact is processed only once when fetching messages. This change improves efficiency and prevents redundant database queries.
2025-05-22 16:53:18 -03:00
Davidson Gomes
78150d0fc6 Merge pull request #1489 from gomessguii/v1.8
fix: Update message model and enhance media handling in Baileys service
2025-05-22 16:51:39 -03:00
Guilherme Gomes
f95727edbf fix: Update message model and enhance media handling in Baileys service
- Changed the type of `message` in `MessageRaw` from `object` to `any` for better flexibility.
- Improved media message handling in `BaileysStartupService` to support base64 encoding for various media types, enhancing webhook functionality.
- Added logging for media message detection to aid in debugging.
2025-05-22 16:50:42 -03:00
Davidson Gomes
2d9ca15d74 docs(changelog): Update CHANGELOG.md 2025-05-12 12:48:52 -03:00
Davidson Gomes
cee6498ea0 feat: Adds method to fetch contacts with last message
- Implements the `fetchContactsWithLastMessage` method in `ChatController` to retrieve contacts and their last messages.
- Updates `ChatRouter` to include the new route that calls the above method.
- Adds logic in `ChannelStartupService` to fetch contacts and their last messages, improving contact management functionality.
2025-05-12 12:48:05 -03:00
Davidson Gomes
86c603b3a1 feat: Updates RabbitMQ with new optimizations and configurations
- Implements Retry and Reconnect system in the integration with RabbitMQ.
- Adds optimizations with parameterized configurations via environment variables (MESSAGE_TTL, MAX_LENGTH and MAX_LENGTH_BYTES).
- Introduces non-persistent messages to reduce disk usage and automatic cleaning of expired messages in queues.
- Updates the version to 1.8.6 in package.json and CHANGELOG.md.
2025-04-30 15:44:12 -03:00
Davidson Gomes
960efcecd5 fix: Retry and Reconnect system in rabbitmq integration 2025-04-28 17:11:19 -03:00
Davidson Gomes
b36c37bf33 version: 1.8.5 2025-02-03 12:32:36 -03:00
Davidson Gomes
cc6adf0ee2 chore: Update Baileys version and re-enable instance management methods
- Switched Baileys dependency to EvolutionAPI repository
- Re-enabled delInstanceTime and deleteTempInstances methods in monitoring services
- Updated CHANGELOG.md to version 1.8.5
2025-01-31 13:52:02 -03:00
Davidson Gomes
6990a2c9c0 test: delete instances 2025-01-29 15:24:40 -03:00
Davidson Gomes
b60215100e feat: Implement retry mechanism for webhook requests in ChannelStartupService
- Added a new private method `retryWebhookRequest` to handle retries for webhook requests, improving reliability in case of failures.
- Updated `sendDataWebhook` method to utilize the new retry mechanism for both local and global webhooks, enhancing error handling and logging.
- Improved logging to provide clearer messages on retry attempts and final error reporting.

These changes enhance the robustness of webhook handling in the application.
2025-01-17 16:04:41 -03:00
Davidson Gomes
b6506dc661 chore: Update Baileys dependency to a new GitHub repository
- Changed the Baileys dependency in package.json from the EvolutionAPI repository to the renatoiub repository.

This update ensures that the project uses the latest version of Baileys from the specified GitHub source.
2025-01-17 15:46:55 -03:00
Davidson Gomes
03ee40388c chore: Bump version to 1.8.4 and update logging in services
- Updated package version from 1.8.2 to 1.8.4 in package.json.
- Refactored instance.controller.ts to allow logout for both 'connecting' and 'open' states, improving instance management.
- Commented out unnecessary logging in channel.service.ts to enhance code clarity.
- Enhanced logging in whatsapp.baileys.service.ts to include instance details in CONNECTION_UPDATE events, improving webhook data sent during connection state changes.

These changes improve versioning, code clarity, and logging functionality across the application.
2025-01-17 15:40:36 -03:00
Davidson Gomes
6ddad8e85a refactor: Simplify queue name generation and update AMQP publish method
- Refactored the queue name generation logic in `channel.service.ts` to improve readability by using a conditional operator.
- Updated the `bindQueue` method to use `queueName` instead of `event` for better clarity in `channel.service.ts`.
- Commented out unused phone number parsing logic in `whatsapp.baileys.service.ts` to clean up the code and improve maintainability.
- Made `isLatest` property optional in the type definition for better flexibility.

These changes enhance code clarity and maintainability across the services.
2024-12-19 11:12:11 -03:00
Davidson Gomes
555fa606ea fix: Update PrivacySetting groupadd type and enhance error handling in fetchInstances route
- Changed the type of `groupadd` in `PrivacySetting` from `WAPrivacyValue` to `any` to allow for more flexible data handling.
- Wrapped the `fetchInstances` route in a try-catch block to improve error handling and logging, ensuring that any errors are logged and a proper error response is returned to the client.

These changes enhance the robustness of the API and improve type flexibility in the DTO.
2024-12-11 15:53:58 -03:00
Davidson Gomes
ca474236b0 feat: Add PREFIX_KEY configuration for RabbitMQ integration
- Introduced a new optional PREFIX_KEY in the RabbitMQ configuration to allow custom queue naming.
- Updated the AMQP server and channel service to utilize PREFIX_KEY when initializing queues.
- Modified the dev environment configuration to include PREFIX_KEY.

This enhancement improves the flexibility of queue naming in RabbitMQ, facilitating better organization and management of queues.
2024-12-11 15:33:01 -03:00
Davidson Gomes
ebd70fe454 Merge pull request #753 from raimartinsb/develop
fix: Correction of media as attachments in chatwoot when using a Meta API Instance and not Baileys
2024-08-12 13:22:12 -03:00
raimartinsb
91f009a617 fix: Correção das mídias como anexo no chatwoot quando usamos uma Instance da Meta e enviando nome do arquivo para a meta 2024-08-12 10:52:56 -03:00
Davidson Gomes
293f6a39c5 chore: Update changelog with fixed issues and maintain compatibility
- Updated CHANGELOG.md with fixed issues in version 1.8.3
- Fixed issue sending group messages when ignore groups enabled
- Fixed groups\_ignore in /instance/create and maintained compatibility
2024-07-12 18:29:35 -03:00
Davidson Gomes
38bf859f43 Fix: Resolve issue with group messages when ignore groups is enabled
This commit resolves an issue where group messages were not being sent when the 'ignore groups' feature was enabled. The modification in the `whatsapp.baileys.service.ts` file ensures that group messages are sent even if the 'ignore groups' feature is enabled. Additionally, the `CHANGELOG.md` file has been updated to reflect this fix in version 1.8.3.

Changes:
- src/api/services/channels/whatsapp.baileys.service.ts
- CHANGELOG.md
2024-07-12 18:27:24 -03:00
Davidson Gomes
1d81c79fe6 Merge pull request #682 from jtapeg/fix-groups-ignore
Fix: groups_ignore in /instance/create and maintaining compatibility
2024-07-12 13:59:20 -03:00
Joao Pedro
c55885b366 Fix: groups_ignore in /instance/create and maintaining compatibility 2024-07-06 21:59:41 -03:00
Davidson Gomes
549ecd8801 Merge pull request #676 from EvolutionAPI/DavidsonGomes-patch-2
Update publish_docker_image_v2.yml
2024-07-03 19:17:56 -03:00
Davidson Gomes
5c6b70f372 Update publish_docker_image_v2.yml 2024-07-03 19:17:47 -03:00
Davidson Gomes
418ca971fa Merge pull request #675 from EvolutionAPI/DavidsonGomes-patch-1
Update publish_docker_image_v2.yml
2024-07-03 18:04:24 -03:00
Davidson Gomes
3d6209618b Update publish_docker_image_v2.yml 2024-07-03 18:04:01 -03:00
Davidson Gomes
05eb58be0e Merge tag '1.8.2' into develop
* Corretion in globall rabbitmq queue name
* Improvement in the use of mongodb database for credentials
* Fixed base64 in webhook for documentWithCaption
* Fixed Generate pairing code
2024-07-03 16:10:42 -03:00
23 changed files with 782 additions and 145 deletions

View File

@@ -1,3 +1,44 @@
# 1.8.7
### Features
* Wavoip integration
# 1.8.6
### Features
* Adds method to fetch contacts with last message
### Fixed
* Retry and Reconnect system in rabbitmq integration
### Feature
* RabbitMQ optimization with parameterized settings via environment variables (MESSAGE_TTL, MAX_LENGTH and MAX_LENGTH_BYTES)
* Non-persistent messages to reduce disk usage
* Automatic cleanup of expired messages in queues
# 1.8.5 (2025-02-03 12:32)
### Fixed
* Update Baileys Version
# 1.8.4 (2025-01-31 10:00)
### Features
* Added prefix key to queue name in RabbitMQ
# 1.8.3 (2024-11-29 10:00)
### Fixed
* Fixed issue sending group messages when ignore groups enabled
* Fixed groups_ignore in /instance/create and maintaining compatibility
# 1.8.2 (2024-07-03 13:50) # 1.8.2 (2024-07-03 13:50)
### Fixed ### Fixed

View File

@@ -1,6 +1,6 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "1.8.2", "version": "1.8.6",
"description": "Rest api for communication with WhatsApp", "description": "Rest api for communication with WhatsApp",
"main": "./dist/src/main.js", "main": "./dist/src/main.js",
"scripts": { "scripts": {
@@ -42,12 +42,12 @@
"homepage": "https://github.com/EvolutionAPI/evolution-api#readme", "homepage": "https://github.com/EvolutionAPI/evolution-api#readme",
"dependencies": { "dependencies": {
"@adiwajshing/keyed-db": "^0.2.4", "@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-sqs": "^3.569.0",
"@ffmpeg-installer/ffmpeg": "^1.1.0", "@ffmpeg-installer/ffmpeg": "^1.1.0",
"@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",
"amqplib": "^0.10.3", "amqplib": "^0.10.3",
"@aws-sdk/client-sqs": "^3.569.0",
"axios": "^1.6.5", "axios": "^1.6.5",
"baileys": "github:EvolutionAPI/Baileys", "baileys": "github:EvolutionAPI/Baileys",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
@@ -84,6 +84,7 @@
"redis": "^4.6.5", "redis": "^4.6.5",
"sharp": "^0.32.2", "sharp": "^0.32.2",
"socket.io": "^4.7.1", "socket.io": "^4.7.1",
"socket.io-client": "^4.8.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",

View File

@@ -86,6 +86,11 @@ export class ChatController {
return await this.waMonitor.waInstances[instanceName].fetchChats(); return await this.waMonitor.waInstances[instanceName].fetchChats();
} }
public async fetchContactsWithLastMessage({ instanceName }: InstanceDto) {
logger.verbose('requested fetchContactsWithLastMessage from ' + instanceName + ' instance');
return await this.waMonitor.waInstances[instanceName].fetchContactsWithLastMessage();
}
public async sendPresence({ instanceName }: InstanceDto, data: SendPresenceDto) { public async sendPresence({ instanceName }: InstanceDto, data: SendPresenceDto) {
logger.verbose('requested sendPresence from ' + instanceName + ' instance'); logger.verbose('requested sendPresence from ' + instanceName + ' instance');
return await this.waMonitor.waInstances[instanceName].sendPresence(data); return await this.waMonitor.waInstances[instanceName].sendPresence(data);

View File

@@ -78,6 +78,7 @@ export class InstanceController {
read_messages, read_messages,
read_status, read_status,
sync_full_history, sync_full_history,
wavoipToken,
websocket_enabled, websocket_enabled,
websocket_events, websocket_events,
rabbitmq_enabled, rabbitmq_enabled,
@@ -396,11 +397,12 @@ export class InstanceController {
const settings: wa.LocalSettings = { const settings: wa.LocalSettings = {
reject_call: reject_call || false, reject_call: reject_call || false,
msg_call: msg_call || '', msg_call: msg_call || '',
groups_ignore: groups_ignore || true, groups_ignore: groups_ignore === undefined ? true : groups_ignore || false,
always_online: always_online || false, always_online: always_online || false,
read_messages: read_messages || false, read_messages: read_messages || false,
read_status: read_status || false, read_status: read_status || false,
sync_full_history: sync_full_history ?? false, sync_full_history: sync_full_history ?? false,
wavoipToken: wavoipToken ?? '',
}; };
this.logger.verbose('settings: ' + JSON.stringify(settings)); this.logger.verbose('settings: ' + JSON.stringify(settings));
@@ -737,16 +739,11 @@ export class InstanceController {
this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance');
const { instance } = await this.connectionState({ instanceName }); const { instance } = await this.connectionState({ instanceName });
if (instance.state === 'open') {
throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected');
}
try { try {
this.waMonitor.waInstances[instanceName]?.removeRabbitmqQueues(); this.waMonitor.waInstances[instanceName]?.removeRabbitmqQueues();
this.waMonitor.waInstances[instanceName]?.clearCacheChatwoot(); this.waMonitor.waInstances[instanceName]?.clearCacheChatwoot();
if (instance.state === 'connecting') { if (instance.state === 'connecting' || instance.state === 'open') {
this.logger.verbose('logging out instance: ' + instanceName);
await this.logout({ instanceName }); await this.logout({ instanceName });
} }

View File

@@ -84,7 +84,7 @@ class PrivacySetting {
status: WAPrivacyValue; status: WAPrivacyValue;
online: WAPrivacyOnlineValue; online: WAPrivacyOnlineValue;
last: WAPrivacyValue; last: WAPrivacyValue;
groupadd: WAPrivacyValue; groupadd: any;
} }
export class PrivacySettingDto { export class PrivacySettingDto {

View File

@@ -21,6 +21,7 @@ export class InstanceDto {
read_messages?: boolean; read_messages?: boolean;
read_status?: boolean; read_status?: boolean;
sync_full_history?: boolean; sync_full_history?: boolean;
wavoipToken?: string;
chatwoot_account_id?: string; chatwoot_account_id?: string;
chatwoot_token?: string; chatwoot_token?: string;
chatwoot_url?: string; chatwoot_url?: string;

View File

@@ -6,4 +6,5 @@ export class SettingsDto {
read_messages?: boolean; read_messages?: boolean;
read_status?: boolean; read_status?: boolean;
sync_full_history?: boolean; sync_full_history?: boolean;
wavoipToken?: string;
} }

View File

@@ -741,7 +741,6 @@ export class ChatwootService {
findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().name_inbox.split('-cwId-')[0]); findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().name_inbox.split('-cwId-')[0]);
} }
if (!findByName) { if (!findByName) {
this.logger.warn('inbox not found'); this.logger.warn('inbox not found');
return null; return null;
@@ -1907,7 +1906,8 @@ export class ChatwootService {
let nameFile: string; let nameFile: string;
const messageBody = body?.message[body?.messageType]; const messageBody = body?.message[body?.messageType];
const originalFilename = messageBody?.fileName || messageBody?.message?.documentMessage?.fileName; const originalFilename =
messageBody?.fileName || messageBody?.filename || messageBody?.message?.documentMessage?.fileName;
if (originalFilename) { if (originalFilename) {
const parsedFile = path.parse(originalFilename); const parsedFile = path.parse(originalFilename);
if (parsedFile.name && parsedFile.ext) { if (parsedFile.name && parsedFile.ext) {

View File

@@ -6,19 +6,53 @@ import { Logger } from '../../../../config/logger.config';
const logger = new Logger('AMQP'); const logger = new Logger('AMQP');
let amqpChannel: amqp.Channel | null = null; let amqpChannel: amqp.Channel | null = null;
let amqpConnection: amqp.Connection | null = null;
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;
const reconnectInterval = 5000; // 5 segundos
type ResolveCallback = () => void;
type RejectCallback = (error: Error) => void;
export const initAMQP = () => { export const initAMQP = () => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
connectToRabbitMQ(resolve, reject);
});
};
const connectToRabbitMQ = (resolve?: ResolveCallback, reject?: RejectCallback) => {
const uri = configService.get<Rabbitmq>('RABBITMQ').URI; const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
amqp.connect(uri, (error, connection) => { amqp.connect(uri, (error, connection) => {
if (error) { if (error) {
reject(error); logger.error(`Failed to connect to RabbitMQ: ${error.message}`);
handleConnectionError(error, resolve, reject);
return; return;
} }
reconnectAttempts = 0;
amqpConnection = connection;
connection.on('error', (err) => {
logger.error(`RabbitMQ connection error: ${err.message}`);
scheduleReconnect();
});
connection.on('close', () => {
logger.warn('RabbitMQ connection closed unexpectedly');
scheduleReconnect();
});
createChannel(connection, resolve, reject);
});
};
const createChannel = (connection: amqp.Connection, resolve?: ResolveCallback, reject?: RejectCallback) => {
connection.createChannel((channelError, channel) => { connection.createChannel((channelError, channel) => {
if (channelError) { if (channelError) {
logger.error(`Failed to create channel: ${channelError.message}`);
if (reject) {
reject(channelError); reject(channelError);
}
return; return;
} }
@@ -29,13 +63,62 @@ export const initAMQP = () => {
autoDelete: false, autoDelete: false,
}); });
channel.on('error', (err) => {
logger.error(`RabbitMQ channel error: ${err.message}`);
amqpChannel = null;
createChannel(connection);
});
channel.on('close', () => {
logger.warn('RabbitMQ channel closed');
amqpChannel = null;
createChannel(connection);
});
amqpChannel = channel; amqpChannel = channel;
logger.info('AMQP initialized'); logger.info('AMQP initialized');
if (resolve) {
resolve(); resolve();
}
}); });
}); };
});
const scheduleReconnect = () => {
if (reconnectAttempts >= maxReconnectAttempts) {
logger.error(`Exceeded maximum ${maxReconnectAttempts} reconnection attempts to RabbitMQ`);
return;
}
amqpChannel = null;
if (amqpConnection) {
try {
amqpConnection.close();
} catch (err) {
// Ignora erro ao fechar conexão que já pode estar fechada
}
amqpConnection = null;
}
reconnectAttempts++;
const delay = reconnectInterval * Math.pow(1.5, reconnectAttempts - 1); // Backoff exponencial
logger.info(`Reconnection attempt ${reconnectAttempts} to RabbitMQ in ${delay}ms`);
setTimeout(() => {
connectToRabbitMQ();
}, delay);
};
const handleConnectionError = (error: Error, resolve?: ResolveCallback, reject?: RejectCallback) => {
if (reject && reconnectAttempts === 0) {
// Na inicialização, rejeitar a Promise se for a primeira tentativa
reject(error);
return;
}
scheduleReconnect();
}; };
export const getAMQP = (): amqp.Channel | null => { export const getAMQP = (): amqp.Channel | null => {
@@ -44,7 +127,12 @@ export const getAMQP = (): amqp.Channel | null => {
export const initGlobalQueues = () => { export const initGlobalQueues = () => {
logger.info('Initializing global queues'); logger.info('Initializing global queues');
const events = configService.get<Rabbitmq>('RABBITMQ').EVENTS; const rabbitmqConfig = configService.get<Rabbitmq>('RABBITMQ');
const events = rabbitmqConfig.EVENTS;
const prefixKey = rabbitmqConfig.PREFIX_KEY;
const messageTtl = rabbitmqConfig.MESSAGE_TTL;
const maxLength = rabbitmqConfig.MAX_LENGTH;
const maxLengthBytes = rabbitmqConfig.MAX_LENGTH_BYTES;
if (!events) { if (!events) {
logger.warn('No events to initialize on AMQP'); logger.warn('No events to initialize on AMQP');
@@ -54,9 +142,15 @@ export const initGlobalQueues = () => {
const eventKeys = Object.keys(events); const eventKeys = Object.keys(events);
eventKeys.forEach((event) => { eventKeys.forEach((event) => {
if (events[event] === false) return; if (events[event] === false) {
return;
}
const queueName =
prefixKey !== ''
? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}`
: `${event.replace(/_/g, '.').toLowerCase()}`;
const queueName = `${event.replace(/_/g, '.').toLowerCase()}`;
const amqp = getAMQP(); const amqp = getAMQP();
const exchangeName = 'evolution_exchange'; const exchangeName = 'evolution_exchange';
@@ -70,6 +164,10 @@ export const initGlobalQueues = () => {
autoDelete: false, autoDelete: false,
arguments: { arguments: {
'x-queue-type': 'quorum', 'x-queue-type': 'quorum',
'x-message-ttl': messageTtl,
'x-max-length': maxLength,
'x-max-length-bytes': maxLengthBytes,
'x-overflow': 'reject-publish',
}, },
}); });
@@ -78,7 +176,14 @@ export const initGlobalQueues = () => {
}; };
export const initQueues = (instanceName: string, events: string[]) => { export const initQueues = (instanceName: string, events: string[]) => {
if (!events || !events.length) return; if (!events || !events.length) {
return;
}
const rabbitmqConfig = configService.get<Rabbitmq>('RABBITMQ');
const messageTtl = rabbitmqConfig.MESSAGE_TTL;
const maxLength = rabbitmqConfig.MAX_LENGTH;
const maxLengthBytes = rabbitmqConfig.MAX_LENGTH_BYTES;
const queues = events.map((event) => { const queues = events.map((event) => {
return `${event.replace(/_/g, '.').toLowerCase()}`; return `${event.replace(/_/g, '.').toLowerCase()}`;
@@ -100,6 +205,10 @@ export const initQueues = (instanceName: string, events: string[]) => {
autoDelete: false, autoDelete: false,
arguments: { arguments: {
'x-queue-type': 'quorum', 'x-queue-type': 'quorum',
'x-message-ttl': messageTtl,
'x-max-length': maxLength,
'x-max-length-bytes': maxLengthBytes,
'x-overflow': 'reject-publish',
}, },
}); });
@@ -108,7 +217,9 @@ export const initQueues = (instanceName: string, events: string[]) => {
}; };
export const removeQueues = (instanceName: string, events: string[]) => { export const removeQueues = (instanceName: string, events: string[]) => {
if (!events || !events.length) return; if (!events || !events.length) {
return;
}
const channel = getAMQP(); const channel = getAMQP();

View File

@@ -8,13 +8,17 @@ const logger = new Logger('Socket');
let io: SocketIO; let io: SocketIO;
const cors = configService.get<Cors>('CORS').ORIGIN; const origin = configService.get<Cors>('CORS').ORIGIN;
const methods = configService.get<Cors>('CORS').METHODS;
const credentials = configService.get<Cors>('CORS').CREDENTIALS;
export const initIO = (httpServer: Server) => { export const initIO = (httpServer: Server) => {
if (configService.get<Websocket>('WEBSOCKET')?.ENABLED) { if (configService.get<Websocket>('WEBSOCKET')?.ENABLED) {
io = new SocketIO(httpServer, { io = new SocketIO(httpServer, {
cors: { cors: {
origin: cors, origin,
methods,
credentials,
}, },
}); });

View File

@@ -1,3 +1,4 @@
import Long from 'long';
import { Schema } from 'mongoose'; import { Schema } from 'mongoose';
import { dbserver } from '../../libs/db.connect'; import { dbserver } from '../../libs/db.connect';
@@ -23,9 +24,9 @@ export class MessageRaw {
key?: Key; key?: Key;
pushName?: string; pushName?: string;
participant?: string; participant?: string;
message?: object; message?: any;
messageType?: string; messageType?: string;
messageTimestamp?: number | Long.Long; messageTimestamp?: number | Long;
owner: string; owner: string;
source?: 'android' | 'web' | 'ios' | 'unknown' | 'desktop'; source?: 'android' | 'web' | 'ios' | 'unknown' | 'desktop';
source_id?: string; source_id?: string;

View File

@@ -11,6 +11,7 @@ export class SettingsRaw {
read_messages?: boolean; read_messages?: boolean;
read_status?: boolean; read_status?: boolean;
sync_full_history?: boolean; sync_full_history?: boolean;
wavoipToken?: string;
} }
const settingsSchema = new Schema<SettingsRaw>({ const settingsSchema = new Schema<SettingsRaw>({
@@ -22,6 +23,7 @@ const settingsSchema = new Schema<SettingsRaw>({
read_messages: { type: Boolean, required: true }, read_messages: { type: Boolean, required: true },
read_status: { type: Boolean, required: true }, read_status: { type: Boolean, required: true },
sync_full_history: { type: Boolean, required: true }, sync_full_history: { type: Boolean, required: true },
wavoipToken: { type: String, required: true },
}); });
export const SettingsModel = dbserver?.model(SettingsRaw.name, settingsSchema, 'settings'); export const SettingsModel = dbserver?.model(SettingsRaw.name, settingsSchema, 'settings');

View File

@@ -253,6 +253,23 @@ export class ChatRouter extends RouterBroker {
return res.status(HttpStatus.OK).json(response); return res.status(HttpStatus.OK).json(response);
}) })
.get(this.routerPath('fetchContactsWithLastMessage'), ...guards, async (req, res) => {
logger.verbose('request received in fetchContactsWithLastMessage');
logger.verbose('request body: ');
logger.verbose(req.body);
logger.verbose('request query: ');
logger.verbose(req.query);
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: null,
ClassRef: InstanceDto,
execute: (instance) => chatController.fetchContactsWithLastMessage(instance),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('sendPresence'), ...guards, async (req, res) => { .post(this.routerPath('sendPresence'), ...guards, async (req, res) => {
logger.verbose('request received in sendPresence'); logger.verbose('request received in sendPresence');
logger.verbose('request body: '); logger.verbose('request body: ');

View File

@@ -99,6 +99,7 @@ export class InstanceRouter extends RouterBroker {
return res.status(HttpStatus.OK).json(response); return res.status(HttpStatus.OK).json(response);
}) })
.get(this.routerPath('fetchInstances', false), ...guards, async (req, res) => { .get(this.routerPath('fetchInstances', false), ...guards, async (req, res) => {
try {
logger.verbose('request received in fetchInstances'); logger.verbose('request received in fetchInstances');
logger.verbose('request body: '); logger.verbose('request body: ');
logger.verbose(req.body); logger.verbose(req.body);
@@ -115,6 +116,11 @@ export class InstanceRouter extends RouterBroker {
}); });
return res.status(HttpStatus.OK).json(response); return res.status(HttpStatus.OK).json(response);
} catch (error) {
logger.error('fetchInstances');
logger.error(error);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error: true, message: error.message });
}
}) })
.post(this.routerPath('setPresence'), ...guards, async (req, res) => { .post(this.routerPath('setPresence'), ...guards, async (req, res) => {
logger.verbose('request received in setPresence'); logger.verbose('request received in setPresence');

View File

@@ -181,6 +181,9 @@ export class ChannelStartupService {
this.localSettings.sync_full_history = data?.sync_full_history; this.localSettings.sync_full_history = data?.sync_full_history;
this.logger.verbose(`Settings sync_full_history: ${this.localSettings.sync_full_history}`); this.logger.verbose(`Settings sync_full_history: ${this.localSettings.sync_full_history}`);
this.localSettings.wavoipToken = data?.wavoipToken;
this.logger.verbose(`Settings wavoipToken: ${this.localSettings.wavoipToken}`);
this.logger.verbose('Settings loaded'); this.logger.verbose('Settings loaded');
} }
@@ -194,8 +197,15 @@ export class ChannelStartupService {
this.logger.verbose(`Settings read_messages: ${data.read_messages}`); this.logger.verbose(`Settings read_messages: ${data.read_messages}`);
this.logger.verbose(`Settings read_status: ${data.read_status}`); this.logger.verbose(`Settings read_status: ${data.read_status}`);
this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`); this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`);
this.logger.verbose(`Settings wavoipToken: ${data.wavoipToken}`);
Object.assign(this.localSettings, data); Object.assign(this.localSettings, data);
this.logger.verbose('Settings set'); this.logger.verbose('Settings set');
// restart instance
if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
this.client.ws.close();
this.client.ws.connect();
}
} }
public async findSettings() { public async findSettings() {
@@ -214,6 +224,7 @@ export class ChannelStartupService {
this.logger.verbose(`Settings read_messages: ${data.read_messages}`); this.logger.verbose(`Settings read_messages: ${data.read_messages}`);
this.logger.verbose(`Settings read_status: ${data.read_status}`); this.logger.verbose(`Settings read_status: ${data.read_status}`);
this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`); this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`);
this.logger.verbose(`Settings wavoipToken: ${data.wavoipToken}`);
return { return {
reject_call: data.reject_call, reject_call: data.reject_call,
msg_call: data.msg_call, msg_call: data.msg_call,
@@ -222,6 +233,7 @@ export class ChannelStartupService {
read_messages: data.read_messages, read_messages: data.read_messages,
read_status: data.read_status, read_status: data.read_status,
sync_full_history: data.sync_full_history, sync_full_history: data.sync_full_history,
wavoipToken: data.wavoipToken,
}; };
} }
@@ -686,7 +698,45 @@ export class ChannelStartupService {
}); });
}; };
public async sendDataWebhook<T = any>(event: Events, data: T, local = true) { private async retryWebhookRequest(
httpService: any,
postData: any,
baseURL: string,
isGlobal = false,
maxRetries = 10,
delaySeconds = 30,
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await httpService.post('', postData);
if (attempt > 1) {
this.logger.verbose(`Webhook ${isGlobal ? 'global' : 'local'} enviado com sucesso na tentativa ${attempt}`);
}
return;
} catch (error) {
if (attempt === maxRetries) {
throw error; // Propaga o erro após todas as tentativas
}
this.logger.warn({
local: `${ChannelStartupService.name}.retryWebhookRequest-${isGlobal ? 'global' : 'local'}`,
message: `Tentativa ${attempt}/${maxRetries} falhou. Próxima tentativa em ${delaySeconds} segundos`,
error: error?.message,
url: baseURL,
});
// Aguarda o delay antes da próxima tentativa
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
}
}
}
public async sendDataWebhook<T = any>(
event: Events,
data: T,
local = true,
integration = ['websocket', 'rabbitmq', 'sqs', 'webhook'],
) {
const webhookGlobal = this.configService.get<Webhook>('WEBHOOK'); const webhookGlobal = this.configService.get<Webhook>('WEBHOOK');
const webhookLocal = this.localWebhook.events; const webhookLocal = this.localWebhook.events;
const websocketLocal = this.localWebsocket.events; const websocketLocal = this.localWebsocket.events;
@@ -706,7 +756,7 @@ export class ChannelStartupService {
const tokenStore = await this.repository.auth.find(this.instanceName); const tokenStore = await this.repository.auth.find(this.instanceName);
const instanceApikey = tokenStore?.apikey || 'Apikey not found'; const instanceApikey = tokenStore?.apikey || 'Apikey not found';
if (rabbitmqEnabled) { if (rabbitmqEnabled && integration.includes('rabbitmq')) {
const amqp = getAMQP(); const amqp = getAMQP();
if (this.localRabbitmq.enabled && amqp) { if (this.localRabbitmq.enabled && amqp) {
if (Array.isArray(rabbitmqLocal) && rabbitmqLocal.includes(we)) { if (Array.isArray(rabbitmqLocal) && rabbitmqLocal.includes(we)) {
@@ -748,7 +798,10 @@ export class ChannelStartupService {
message['apikey'] = instanceApikey; message['apikey'] = instanceApikey;
} }
await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message))); await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message)), {
persistent: false,
expiration: this.configService.get<Rabbitmq>('RABBITMQ').MESSAGE_TTL.toString(),
});
if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) { if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) {
const logData = { const logData = {
@@ -778,6 +831,7 @@ export class ChannelStartupService {
if (rabbitmqGlobal && rabbitmqEvents[we] && amqp) { if (rabbitmqGlobal && rabbitmqEvents[we] && amqp) {
const exchangeName = 'evolution_exchange'; const exchangeName = 'evolution_exchange';
const prefixKey = this.configService.get<Rabbitmq>('RABBITMQ').PREFIX_KEY;
let retry = 0; let retry = 0;
@@ -788,7 +842,9 @@ export class ChannelStartupService {
autoDelete: false, autoDelete: false,
}); });
const queueName = event; const queueName = prefixKey
? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}`
: event.replace(/_/g, '.').toLowerCase();
await amqp.assertQueue(queueName, { await amqp.assertQueue(queueName, {
durable: true, durable: true,
@@ -798,7 +854,7 @@ export class ChannelStartupService {
}, },
}); });
await amqp.bindQueue(queueName, exchangeName, event); await amqp.bindQueue(queueName, exchangeName, queueName);
const message = { const message = {
event, event,
@@ -812,7 +868,11 @@ export class ChannelStartupService {
if (expose && instanceApikey) { if (expose && instanceApikey) {
message['apikey'] = instanceApikey; message['apikey'] = instanceApikey;
} }
await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message)));
await amqp.publish(exchangeName, queueName, Buffer.from(JSON.stringify(message)), {
persistent: false,
expiration: this.configService.get<Rabbitmq>('RABBITMQ').MESSAGE_TTL.toString(),
});
if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) { if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) {
const logData = { const logData = {
@@ -841,7 +901,7 @@ export class ChannelStartupService {
} }
} }
if (this.localSqs.enabled) { if (this.localSqs.enabled && integration.includes('sqs')) {
const sqs = getSQS(); const sqs = getSQS();
if (sqs) { if (sqs) {
@@ -911,7 +971,7 @@ export class ChannelStartupService {
} }
} }
if (this.configService.get<Websocket>('WEBSOCKET')?.ENABLED) { if (this.configService.get<Websocket>('WEBSOCKET')?.ENABLED && integration.includes('websocket')) {
this.logger.verbose('Sending data to websocket on channel: ' + this.instance.name); this.logger.verbose('Sending data to websocket on channel: ' + this.instance.name);
const io = getIO(); const io = getIO();
@@ -985,7 +1045,7 @@ export class ChannelStartupService {
const globalApiKey = this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY; const globalApiKey = this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;
if (local) { if (local) {
if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { if (Array.isArray(webhookLocal) && webhookLocal.includes(we) && integration.includes('webhook')) {
this.logger.verbose('Sending data to webhook local'); this.logger.verbose('Sending data to webhook local');
let baseURL: string; let baseURL: string;
@@ -1033,12 +1093,13 @@ export class ChannelStartupService {
postData['apikey'] = instanceApikey; postData['apikey'] = instanceApikey;
} }
await httpService.post('', postData); await this.retryWebhookRequest(httpService, postData, baseURL);
} }
} catch (error) { } catch (error) {
this.logger.error({ this.logger.error({
local: ChannelStartupService.name + '.sendDataWebhook-local', local: ChannelStartupService.name + '.sendDataWebhook-local',
message: error?.message, message: 'Todas as tentativas de envio do webhook local falharam',
lastError: error?.message,
hostName: error?.hostname, hostName: error?.hostname,
syscall: error?.syscall, syscall: error?.syscall,
code: error?.code, code: error?.code,
@@ -1052,7 +1113,7 @@ export class ChannelStartupService {
} }
} }
if (webhookGlobal.GLOBAL?.ENABLED) { if (webhookGlobal.GLOBAL?.ENABLED && integration.includes('webhook')) {
if (webhookGlobal.EVENTS[we]) { if (webhookGlobal.EVENTS[we]) {
this.logger.verbose('Sending data to webhook global'); this.logger.verbose('Sending data to webhook global');
const globalWebhook = this.configService.get<Webhook>('WEBHOOK').GLOBAL; const globalWebhook = this.configService.get<Webhook>('WEBHOOK').GLOBAL;
@@ -1104,12 +1165,13 @@ export class ChannelStartupService {
postData['apikey'] = globalApiKey; postData['apikey'] = globalApiKey;
} }
await httpService.post('', postData); await this.retryWebhookRequest(httpService, postData, globalURL, true);
} }
} catch (error) { } catch (error) {
this.logger.error({ this.logger.error({
local: ChannelStartupService.name + '.sendDataWebhook-global', local: ChannelStartupService.name + '.sendDataWebhook-global',
message: error?.message, message: 'Todas as tentativas de envio do webhook global falharam',
lastError: error?.message,
hostName: error?.hostname, hostName: error?.hostname,
syscall: error?.syscall, syscall: error?.syscall,
code: error?.code, code: error?.code,
@@ -1283,4 +1345,36 @@ export class ChannelStartupService {
this.logger.verbose('Fetching chats'); this.logger.verbose('Fetching chats');
return await this.repository.chat.find({ where: { owner: this.instance.name } }); return await this.repository.chat.find({ where: { owner: this.instance.name } });
} }
public async fetchContactsWithLastMessage() {
this.logger.verbose('Searching for contacts with last message');
const contacts = await this.repository.contact.find({ where: { owner: this.instance.name } });
const result = [];
const seenIds = new Set();
for (const contact of contacts) {
if (seenIds.has(contact.id)) {
continue;
}
seenIds.add(contact.id);
const messages = await this.repository.message.find({
where: {
owner: this.instance.name,
key: { remoteJid: contact.id },
},
limit: 1,
});
if (messages && messages.length > 0) {
result.push({
id: contact.id,
pushName: contact?.pushName ?? null,
profilePictureUrl: contact?.profilePictureUrl ?? null,
owner: contact.owner,
lastMessage: messages[0],
});
}
}
return result;
}
} }

View File

@@ -0,0 +1,78 @@
import { BinaryNode, Contact, JidWithDevice, proto, WAConnectionState } from 'baileys';
export interface ServerToClientEvents {
withAck: (d: string, callback: (e: number) => void) => void;
onWhatsApp: onWhatsAppType;
profilePictureUrl: ProfilePictureUrlType;
assertSessions: AssertSessionsType;
createParticipantNodes: CreateParticipantNodesType;
getUSyncDevices: GetUSyncDevicesType;
generateMessageTag: GenerateMessageTagType;
sendNode: SendNodeType;
'signalRepository:decryptMessage': SignalRepositoryDecryptMessageType;
}
export interface ClientToServerEvents {
init: (
me: Contact | undefined,
account: proto.IADVSignedDeviceIdentity | undefined,
status: WAConnectionState,
) => void;
'CB:call': (packet: any) => void;
'CB:ack,class:call': (packet: any) => void;
'connection.update:status': (
me: Contact | undefined,
account: proto.IADVSignedDeviceIdentity | undefined,
status: WAConnectionState,
) => void;
'connection.update:qr': (qr: string) => void;
}
export type onWhatsAppType = (jid: string, callback: onWhatsAppCallback) => void;
export type onWhatsAppCallback = (
response: {
exists: boolean;
jid: string;
}[],
) => void;
export type ProfilePictureUrlType = (
jid: string,
type: 'image' | 'preview',
timeoutMs: number | undefined,
callback: ProfilePictureUrlCallback,
) => void;
export type ProfilePictureUrlCallback = (response: string | undefined) => void;
export type AssertSessionsType = (jids: string[], force: boolean, callback: AssertSessionsCallback) => void;
export type AssertSessionsCallback = (response: boolean) => void;
export type CreateParticipantNodesType = (
jids: string[],
message: any,
extraAttrs: any,
callback: CreateParticipantNodesCallback,
) => void;
export type CreateParticipantNodesCallback = (nodes: any, shouldIncludeDeviceIdentity: boolean) => void;
export type GetUSyncDevicesType = (
jids: string[],
useCache: boolean,
ignoreZeroDevices: boolean,
callback: GetUSyncDevicesTypeCallback,
) => void;
export type GetUSyncDevicesTypeCallback = (jids: JidWithDevice[]) => void;
export type GenerateMessageTagType = (callback: GenerateMessageTagTypeCallback) => void;
export type GenerateMessageTagTypeCallback = (response: string) => void;
export type SendNodeType = (stanza: BinaryNode, callback: SendNodeTypeCallback) => void;
export type SendNodeTypeCallback = (response: boolean) => void;
export type SignalRepositoryDecryptMessageType = (
jid: string,
type: 'pkmsg' | 'msg',
ciphertext: Buffer,
callback: SignalRepositoryDecryptMessageCallback,
) => void;
export type SignalRepositoryDecryptMessageCallback = (response: any) => void;

View File

@@ -0,0 +1,181 @@
import { ConnectionState, WAConnectionState, WASocket } from 'baileys';
import { io, Socket } from 'socket.io-client';
import { ClientToServerEvents, ServerToClientEvents } from './transport.type';
let baileys_connection_state: WAConnectionState = 'close';
export const useVoiceCallsBaileys = async (
wavoip_token: string,
baileys_sock: WASocket,
status?: WAConnectionState,
logger?: boolean,
) => {
baileys_connection_state = status ?? 'close';
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('https://devices.wavoip.com/baileys', {
transports: ['websocket'],
path: `/${wavoip_token}/websocket`,
});
socket.on('connect', () => {
if (logger) console.log('[*] - Wavoip connected', socket.id);
socket.emit(
'init',
baileys_sock.authState.creds.me,
baileys_sock.authState.creds.account,
baileys_connection_state,
);
});
socket.on('disconnect', () => {
if (logger) console.log('[*] - Wavoip disconnect');
});
socket.on('connect_error', (error) => {
if (socket.active) {
if (logger)
console.log(
'[*] - Wavoip connection error temporary failure, the socket will automatically try to reconnect',
error,
);
} else {
if (logger) console.log('[*] - Wavoip connection error', error.message);
}
});
socket.on('onWhatsApp', async (jid, callback) => {
try {
const response: any = await baileys_sock.onWhatsApp(jid);
callback(response);
if (logger) console.log('[*] Success on call onWhatsApp function', response, jid);
} catch (error) {
if (logger) console.error('[*] Error on call onWhatsApp function', error);
}
});
socket.on('profilePictureUrl', async (jid, type, timeoutMs, callback) => {
try {
const response = await baileys_sock.profilePictureUrl(jid, type, timeoutMs);
callback(response);
if (logger) console.log('[*] Success on call profilePictureUrl function', response);
} catch (error) {
if (logger) console.error('[*] Error on call profilePictureUrl function', error);
}
});
socket.on('assertSessions', async (jids, force, callback) => {
try {
const response = await baileys_sock.assertSessions(jids, force);
callback(response);
if (logger) console.log('[*] Success on call assertSessions function', response);
} catch (error) {
if (logger) console.error('[*] Error on call assertSessions function', error);
}
});
socket.on('createParticipantNodes', async (jids, message, extraAttrs, callback) => {
try {
const response = await baileys_sock.createParticipantNodes(jids, message, extraAttrs);
callback(response, true);
if (logger) console.log('[*] Success on call createParticipantNodes function', response);
} catch (error) {
if (logger) console.error('[*] Error on call createParticipantNodes function', error);
}
});
socket.on('getUSyncDevices', async (jids, useCache, ignoreZeroDevices, callback) => {
try {
const response = await baileys_sock.getUSyncDevices(jids, useCache, ignoreZeroDevices);
callback(response);
if (logger) console.log('[*] Success on call getUSyncDevices function', response);
} catch (error) {
if (logger) console.error('[*] Error on call getUSyncDevices function', error);
}
});
socket.on('generateMessageTag', async (callback) => {
try {
const response = await baileys_sock.generateMessageTag();
callback(response);
if (logger) console.log('[*] Success on call generateMessageTag function', response);
} catch (error) {
if (logger) console.error('[*] Error on call generateMessageTag function', error);
}
});
socket.on('sendNode', async (stanza, callback) => {
try {
console.log('sendNode', JSON.stringify(stanza));
const response = await baileys_sock.sendNode(stanza);
callback(true);
if (logger) console.log('[*] Success on call sendNode function', response);
} catch (error) {
if (logger) console.error('[*] Error on call sendNode function', error);
}
});
socket.on('signalRepository:decryptMessage', async (jid, type, ciphertext, callback) => {
try {
const response = await baileys_sock.signalRepository.decryptMessage({
jid: jid,
type: type,
ciphertext: ciphertext,
});
callback(response);
if (logger) console.log('[*] Success on call signalRepository:decryptMessage function', response);
} catch (error) {
if (logger) console.error('[*] Error on call signalRepository:decryptMessage function', error);
}
});
// we only use this connection data to inform the webphone that the device is connected and creeds account to generate e2e whatsapp key for make call packets
baileys_sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
const { connection } = update;
if (connection) {
baileys_connection_state = connection;
socket
.timeout(1000)
.emit(
'connection.update:status',
baileys_sock.authState.creds.me,
baileys_sock.authState.creds.account,
connection,
);
}
if (update.qr) {
socket.timeout(1000).emit('connection.update:qr', update.qr);
}
});
baileys_sock.ws.on('CB:call', (packet) => {
if (logger) console.log('[*] Signling received');
socket.volatile.timeout(1000).emit('CB:call', packet);
});
baileys_sock.ws.on('CB:ack,class:call', (packet) => {
if (logger) console.log('[*] Signling ack received');
socket.volatile.timeout(1000).emit('CB:ack,class:call', packet);
});
return socket;
};

View File

@@ -26,7 +26,6 @@ import makeWASocket, {
MessageUpsertType, MessageUpsertType,
MiscMessageGenerationOptions, MiscMessageGenerationOptions,
ParticipantAction, ParticipantAction,
PHONENUMBER_MCC,
prepareWAMessageMedia, prepareWAMessageMedia,
proto, proto,
useMultiFileAuthState, useMultiFileAuthState,
@@ -45,7 +44,6 @@ import { isBase64, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
// import ffmpeg from 'fluent-ffmpeg'; // import ffmpeg from 'fluent-ffmpeg';
import fs, { existsSync, readFileSync } from 'fs'; import fs, { existsSync, readFileSync } from 'fs';
import { parsePhoneNumber } from 'libphonenumber-js';
import Long from 'long'; import Long from 'long';
import NodeCache from 'node-cache'; import NodeCache from 'node-cache';
import { getMIMEType } from 'node-mime-types'; import { getMIMEType } from 'node-mime-types';
@@ -66,6 +64,7 @@ import {
Log, Log,
ProviderSession, ProviderSession,
QrCode, QrCode,
Websocket,
} from '../../../config/env.config'; } from '../../../config/env.config';
import { INSTANCE_DIR } from '../../../config/path.config'; import { INSTANCE_DIR } from '../../../config/path.config';
import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../../exceptions'; import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../../exceptions';
@@ -132,6 +131,7 @@ import { waMonitor } from '../../server.module';
import { Events, MessageSubtype, TypeMediaMessage, wa } from '../../types/wa.types'; import { Events, MessageSubtype, TypeMediaMessage, wa } from '../../types/wa.types';
import { CacheService } from './../cache.service'; import { CacheService } from './../cache.service';
import { ChannelStartupService } from './../channel.service'; import { ChannelStartupService } from './../channel.service';
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine()); const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
@@ -381,12 +381,6 @@ export class BaileysStartupService extends ChannelStartupService {
state: connection, state: connection,
statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200,
}; };
this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE');
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
...this.stateConnection,
});
} }
if (connection === 'close') { if (connection === 'close') {
@@ -419,6 +413,15 @@ export class BaileysStartupService extends ChannelStartupService {
this.client?.ws?.close(); this.client?.ws?.close();
this.client.end(new Error('Close connection')); this.client.end(new Error('Close connection'));
this.logger.verbose('Connection closed'); this.logger.verbose('Connection closed');
this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE');
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
wuid: this.instance.wuid,
profileName: await this.getProfileName(),
profilePictureUrl: this.instance.profilePictureUrl,
...this.stateConnection,
});
} }
} }
@@ -448,13 +451,34 @@ export class BaileysStartupService extends ChannelStartupService {
{ {
instance: this.instance.name, instance: this.instance.name,
status: 'open', status: 'open',
wuid: this.instance.wuid,
profileName: await this.getProfileName(),
profilePictureUrl: this.instance.profilePictureUrl,
}, },
); );
} }
this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE');
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
wuid: this.instance.wuid,
profileName: await this.getProfileName(),
profilePictureUrl: this.instance.profilePictureUrl,
...this.stateConnection,
});
} }
if (connection === 'connecting') { if (connection === 'connecting') {
if (this.mobile) this.sendMobileCode(); if (this.mobile) this.sendMobileCode();
this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE');
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
wuid: this.instance.wuid,
profileName: await this.getProfileName(),
profilePictureUrl: this.instance.profilePictureUrl,
...this.stateConnection,
});
} }
} }
@@ -646,10 +670,32 @@ export class BaileysStartupService extends ChannelStartupService {
this.logger.verbose('Socket created'); this.logger.verbose('Socket created');
if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
useVoiceCallsBaileys(this.localSettings.wavoipToken, this.client, this.connectionStatus.state as any, true);
}
this.eventHandler(); this.eventHandler();
this.logger.verbose('Socket event handler initialized'); this.logger.verbose('Socket event handler initialized');
this.client.ws.on('CB:call', (packet) => {
console.log('CB:call', packet);
const payload = {
event: 'CB:call',
packet: packet,
};
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
});
this.client.ws.on('CB:ack,class:call', (packet) => {
console.log('CB:ack,class:call', packet);
const payload = {
event: 'CB:ack,class:call',
packet: packet,
};
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
});
this.phoneNumber = number; this.phoneNumber = number;
return this.client; return this.client;
@@ -676,61 +722,51 @@ export class BaileysStartupService extends ChannelStartupService {
} }
private async sendMobileCode() { private async sendMobileCode() {
const { registration } = this.client.authState.creds || null; // const { registration } = this.client.authState.creds || null;
// let phoneNumber = registration.phoneNumber || this.phoneNumber;
let phoneNumber = registration.phoneNumber || this.phoneNumber; // if (!phoneNumber.startsWith('+')) {
// phoneNumber = '+' + phoneNumber;
if (!phoneNumber.startsWith('+')) { // }
phoneNumber = '+' + phoneNumber; // if (!phoneNumber) {
} // this.logger.error('Phone number not found');
// return;
if (!phoneNumber) { // }
this.logger.error('Phone number not found'); // const parsedPhoneNumber = parsePhoneNumber(phoneNumber);
return; // if (!parsedPhoneNumber?.isValid()) {
} // this.logger.error('Phone number invalid');
// return;
const parsedPhoneNumber = parsePhoneNumber(phoneNumber); // }
// registration.phoneNumber = parsedPhoneNumber.format('E.164');
if (!parsedPhoneNumber?.isValid()) { // registration.phoneNumberCountryCode = parsedPhoneNumber.countryCallingCode;
this.logger.error('Phone number invalid'); // registration.phoneNumberNationalNumber = parsedPhoneNumber.nationalNumber;
return; // const mcc = await PHONENUMBER_MCC[parsedPhoneNumber.countryCallingCode];
} // if (!mcc) {
// this.logger.error('MCC not found');
registration.phoneNumber = parsedPhoneNumber.format('E.164'); // return;
registration.phoneNumberCountryCode = parsedPhoneNumber.countryCallingCode; // }
registration.phoneNumberNationalNumber = parsedPhoneNumber.nationalNumber; // registration.phoneNumberMobileCountryCode = mcc;
// registration.method = 'sms';
const mcc = await PHONENUMBER_MCC[parsedPhoneNumber.countryCallingCode]; // try {
if (!mcc) { // const response = await this.client.requestRegistrationCode(registration);
this.logger.error('MCC not found'); // if (['ok', 'sent'].includes(response?.status)) {
return; // this.logger.verbose('Registration code sent successfully');
} // return response;
// }
registration.phoneNumberMobileCountryCode = mcc; // } catch (error) {
registration.method = 'sms'; // this.logger.error(error);
// }
try {
const response = await this.client.requestRegistrationCode(registration);
if (['ok', 'sent'].includes(response?.status)) {
this.logger.verbose('Registration code sent successfully');
return response;
}
} catch (error) {
this.logger.error(error);
}
} }
public async receiveMobileCode(code: string) { public async receiveMobileCode(code: string) {
await this.client console.log(code);
.register(code.replace(/["']/g, '').trim().toLowerCase()) // await this.client
.then(async () => { // .register(code.replace(/["']/g, '').trim().toLowerCase())
this.logger.verbose('Registration code received successfully'); // .then(async () => {
}) // this.logger.verbose('Registration code received successfully');
.catch((error) => { // })
this.logger.error(error); // .catch((error) => {
}); // this.logger.error(error);
// });
} }
public async reloadConnection(): Promise<WASocket> { public async reloadConnection(): Promise<WASocket> {
@@ -897,7 +933,7 @@ export class BaileysStartupService extends ChannelStartupService {
chats: Chat[]; chats: Chat[];
contacts: Contact[]; contacts: Contact[];
messages: proto.IWebMessageInfo[]; messages: proto.IWebMessageInfo[];
isLatest: boolean; isLatest?: boolean;
}, },
database: Database, database: Database,
) => { ) => {
@@ -1017,7 +1053,7 @@ export class BaileysStartupService extends ChannelStartupService {
await this.contactHandle['contacts.upsert']( await this.contactHandle['contacts.upsert'](
contacts contacts
.filter((c) => !!c.notify ?? !!c.name) .filter((c) => !!c.notify || !!c.name)
.map((c) => ({ .map((c) => ({
id: c.id, id: c.id,
name: c.name ?? c.notify, name: c.name ?? c.notify,
@@ -1103,7 +1139,12 @@ export class BaileysStartupService extends ChannelStartupService {
const contentMsg = received?.message[getContentType(received.message)] as any; const contentMsg = received?.message[getContentType(received.message)] as any;
if (this.localWebhook.webhook_base64 === true && isMedia) { if (
(this.localWebhook.webhook_base64 === true ||
(this.configService.get<Websocket>('WEBSOCKET').GLOBAL_EVENTS === true &&
this.configService.get<Websocket>('WEBSOCKET').ENABLED === true)) &&
isMedia
) {
const buffer = await downloadMediaMessage( const buffer = await downloadMediaMessage(
{ key: received.key, message: received?.message }, { key: received.key, message: received?.message },
'buffer', 'buffer',
@@ -1944,6 +1985,32 @@ export class BaileysStartupService extends ChannelStartupService {
source: getDevice(messageSent.key.id), source: getDevice(messageSent.key.id),
}; };
const isMedia =
messageRaw.messageType === 'imageMessage' ||
messageRaw.messageType === 'videoMessage' ||
messageRaw.messageType === 'documentMessage' ||
messageRaw.messageType === 'audioMessage';
console.log('isMedia', isMedia);
if (
(this.localWebhook.webhook_base64 === true ||
(this.configService.get<Websocket>('WEBSOCKET').GLOBAL_EVENTS === true &&
this.configService.get<Websocket>('WEBSOCKET').ENABLED === true)) &&
isMedia
) {
const buffer = await downloadMediaMessage(
{ key: messageRaw.key, message: messageRaw?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: this.client.updateMediaMessage,
},
);
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
}
this.logger.log(messageRaw); this.logger.log(messageRaw);
this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); this.logger.verbose('Sending data to webhook in event SEND_MESSAGE');
@@ -2590,7 +2657,7 @@ export class BaileysStartupService extends ChannelStartupService {
const group = await this.findGroup({ groupJid: jid }, 'inner'); const group = await this.findGroup({ groupJid: jid }, 'inner');
if (!group) { if (!group) {
new OnWhatsAppDto(jid, false, number); return new OnWhatsAppDto(jid, false, number);
} }
return new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject); return new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject);
@@ -3260,10 +3327,6 @@ export class BaileysStartupService extends ChannelStartupService {
} }
public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') {
if (this.localSettings.groups_ignore === true) {
return;
}
this.logger.verbose('Fetching group'); this.logger.verbose('Fetching group');
try { try {
const group = await this.client.groupMetadata(id.groupJid); const group = await this.client.groupMetadata(id.groupJid);

View File

@@ -743,6 +743,7 @@ export class BusinessStartupService extends ChannelStartupService {
[message['type']]: message['id'], [message['type']]: message['id'],
preview_url: linkPreview, preview_url: linkPreview,
caption: message['caption'], caption: message['caption'],
filename: message['fileName'],
}, },
}; };
quoted ? (content.context = { message_id: quoted.id }) : content; quoted ? (content.context = { message_id: quoted.id }) : content;
@@ -1212,7 +1213,7 @@ export class BusinessStartupService extends ChannelStartupService {
try { try {
const msg = data.message; const msg = data.message;
this.logger.verbose('Getting base64 from media message'); this.logger.verbose('Getting base64 from media message');
const messageType = msg.messageType + 'Message'; const messageType = msg.messageType.includes('Message') ? msg.messageType : msg.messageType + 'Message';
const mediaMessage = msg.message[messageType]; const mediaMessage = msg.message[messageType];
this.logger.verbose('Media message downloaded'); this.logger.verbose('Media message downloaded');

View File

@@ -83,6 +83,7 @@ export declare namespace wa {
read_messages?: boolean; read_messages?: boolean;
read_status?: boolean; read_status?: boolean;
sync_full_history?: boolean; sync_full_history?: boolean;
wavoipToken?: string;
}; };
export type LocalWebsocket = { export type LocalWebsocket = {

View File

@@ -104,7 +104,11 @@ export type Rabbitmq = {
ENABLED: boolean; ENABLED: boolean;
URI: string; URI: string;
EXCHANGE_NAME: string; EXCHANGE_NAME: string;
PREFIX_KEY?: string;
GLOBAL_ENABLED: boolean; GLOBAL_ENABLED: boolean;
MESSAGE_TTL: number;
MAX_LENGTH: number;
MAX_LENGTH_BYTES: number;
EVENTS: EventsRabbitmq; EVENTS: EventsRabbitmq;
}; };
@@ -323,7 +327,11 @@ export class ConfigService {
ENABLED: process.env?.RABBITMQ_ENABLED === 'true', ENABLED: process.env?.RABBITMQ_ENABLED === 'true',
GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true', GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true',
EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange', EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange',
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY || '',
URI: process.env.RABBITMQ_URI || '', URI: process.env.RABBITMQ_URI || '',
MESSAGE_TTL: Number.parseInt(process.env?.RABBITMQ_MESSAGE_TTL) || 604800,
MAX_LENGTH: Number.parseInt(process.env?.RABBITMQ_MAX_LENGTH) || 10000,
MAX_LENGTH_BYTES: Number.parseInt(process.env?.RABBITMQ_MAX_LENGTH_BYTES) || 8192,
EVENTS: { EVENTS: {
APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true', APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true',
INSTANCE_CREATE: process.env?.RABBITMQ_EVENTS_INSTANCE_CREATE === 'true', INSTANCE_CREATE: process.env?.RABBITMQ_EVENTS_INSTANCE_CREATE === 'true',

View File

@@ -89,7 +89,14 @@ RABBITMQ:
ENABLED: false ENABLED: false
URI: "amqp://guest:guest@localhost:5672" URI: "amqp://guest:guest@localhost:5672"
EXCHANGE_NAME: evolution_exchange EXCHANGE_NAME: evolution_exchange
PREFIX_KEY: evolution
GLOBAL_ENABLED: true GLOBAL_ENABLED: true
# Tempo de vida das mensagens: 1 hora em milissegundos (3600000 = 60 * 60 * 1000)
MESSAGE_TTL: 3600000
# Limite máximo de mensagens por fila (quando atingido, novas mensagens são rejeitadas)
MAX_LENGTH: 1000
# Tamanho máximo em bytes permitido para filas: 10MB (10485760 = 10 * 1024 * 1024)
MAX_LENGTH_BYTES: 10485760
EVENTS: EVENTS:
APPLICATION_STARTUP: false APPLICATION_STARTUP: false
INSTANCE_CREATE: false INSTANCE_CREATE: false

View File

@@ -1002,9 +1002,26 @@ export const settingsSchema: JSONSchema7 = {
read_messages: { type: 'boolean', enum: [true, false] }, read_messages: { type: 'boolean', enum: [true, false] },
read_status: { type: 'boolean', enum: [true, false] }, read_status: { type: 'boolean', enum: [true, false] },
sync_full_history: { type: 'boolean', enum: [true, false] }, sync_full_history: { type: 'boolean', enum: [true, false] },
wavoipToken: { type: 'string' },
}, },
required: ['reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status', 'sync_full_history'], required: [
...isNotEmpty('reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status', 'sync_full_history'), 'reject_call',
'groups_ignore',
'always_online',
'read_messages',
'read_status',
'sync_full_history',
'wavoipToken',
],
...isNotEmpty(
'reject_call',
'groups_ignore',
'always_online',
'read_messages',
'read_status',
'sync_full_history',
'wavoipToken',
),
}; };
export const websocketSchema: JSONSchema7 = { export const websocketSchema: JSONSchema7 = {