Compare commits

...

59 Commits
2.3.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
Davidson Gomes
b8fe5603fd Merge branch 'release/1.8.2' 2024-07-03 16:10:38 -03:00
Davidson Gomes
762453c0e3 chore: Add workflow to publish Docker image
A new untracked file '.github/workflows/publish\_docker\_image\_latest.yml' was added. This workflow will handle the process of publishing the latest Docker image. This change allows for easier and more automated deployment of the Node.js project.
2024-07-03 16:10:23 -03:00
Davidson Gomes
af1b5caa29 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:09:25 -03:00
Davidson Gomes
50b3379d88 Merge branch 'release/1.8.2' 2024-07-03 16:09:17 -03:00
Davidson Gomes
c9ac5984ec 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:06:24 -03:00
Davidson Gomes
29ba63d621 Merge branch 'release/1.8.2' 2024-07-03 16:06:16 -03:00
Davidson Gomes
2ce64af502 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 13:51:36 -03:00
Davidson Gomes
28c517a3d5 Merge branch 'release/1.8.2' 2024-07-03 13:51:28 -03:00
Davidson Gomes
18ebe27bc3 "chore: Updated package and documentation versions to v1.8.2"
Explanation:
This commit updates the package and documentation versions to v1.8.2. The package.json and swagger.yaml files were modified accordingly. These changes are mainly for maintenance purposes and do not affect the functionality of the application.
2024-07-03 13:51:16 -03:00
Davidson Gomes
0f7a39a08f chore(changelog): Update changelog to v1.8.2
This commit updates the changelog to reflect the release of version 1.8.2. The date of the release has been updated in the header of the changelog. No functional changes were made in this release.

Modified: CHANGELOG.md
2024-07-03 13:50:30 -03:00
Davidson Gomes
a8121d7fe6 fix: Correction in global RabbitMQ queue name
Fix global RabbitMQ queue name in `channel.service.ts` and update CHANGELOG.md.
The queue name has been changed from `transformedWe` to `event`.
This fix prevents queue errors and ensures correct functionality of inter-service communication.
2024-07-03 13:47:25 -03:00
Davidson Gomes
b63b7b0b7b fix: Generate pairing code and update Baileys repository
Update package.json to use the new Baileys repository and modify the Whatsapp Baileys service to generate a pairing code. This change fixes the issue with the previous Baileys repository and improves the pairing process for Whatsapp.

Changes:
- Update package.json to use the new Baileys repository
- Modify Whatsapp Baileys service to generate a pairing code
- Fix issue with previous Baileys repository
- Improve pairing process for Whatsapp
2024-07-03 13:45:05 -03:00
Davidson Gomes
14ea5d959f Merge pull request #662 from drauber/cwId_missing
Add -cwId- when findByName
2024-06-26 10:32:09 -03:00
Douglas Rauber at Nitro
86b2999fcf Add -cwId- when findByName 2024-06-24 10:37:54 -03:00
Davidson Gomes
f5bd11fc19 feat: add support for documentWithCaptionMessage in WhatsApp Baileys service
Modified whatsapp.baileys.service.ts to include handling for documentWithCaptionMessage. This change ensures that messages with documents having captions are properly processed, enhancing the service's message handling capabilities. No impact on existing functionalities.
2024-06-18 11:34:47 -03:00
Davidson Gomes
c060d330de fix: normalize event names in channel.service.ts
Normalized event names by replacing underscores with dots and converting to lowercase. This ensures consistent naming conventions and prevents potential issues with queue bindings.
2024-06-18 11:32:50 -03:00
Davidson Gomes
1e320f7904 fix: update use-multi-file-auth-state-db.ts to handle edge cases
Refactored the use-multi-file-auth-state-db.ts to better handle edge cases in multi-file authentication state management. This change improves reliability and ensures more robust error handling, reducing potential issues during authentication.
2024-06-17 11:19:22 -03:00
Davidson Gomes
975b3ee528 fix: reorder imports to resolve linting issues
Reordered imports in multiple files to resolve linting issues and improve code readability. This change does not impact the functionality but ensures the code adheres to the project's coding standards.
2024-06-16 17:19:45 -03:00
Davidson Gomes
5b47bc9ef0 feat: update dependencies and improve caching logic
Updated package.json to include latest dependencies. Enhanced caching logic in cache.service.ts and rediscache.ts for better performance. Improved DTOs in chat.dto.ts, instance.dto.ts, and sendMessage.dto.ts for more robust data handling. Refined instance.controller.ts and chatwoot.service.ts to streamline API integrations. Adjusted authentication state management in use-multi-file-auth-state-db.ts, use-multi-file-auth-state-provider-files.ts, and use-multi-file-auth-state-redis-db.ts. These changes aim to optimize the system's performance and reliability.
2024-06-16 17:17:55 -03:00
Davidson Gomes
053a7981d1 fix: update default values for WA_BUSINESS environment variables
Updated default values for WA_BUSINESS_TOKEN_WEBHOOK, WA_BUSINESS_URL, and WA_BUSINESS_VERSION in env.config.ts to 'evolution', 'https://graph.facebook.com', and 'v19.0' respectively. This change ensures that the application uses more appropriate defaults if environment variables are not set, improving reliability and consistency.
2024-06-13 18:51:18 -03:00
Davidson Gomes
5f1f025d65 Merge tag '1.8.1' into develop
v
2024-06-12 19:49:40 -03:00
Davidson Gomes
dfb003fd72 Merge branch 'release/1.8.1' 2024-06-12 19:49:37 -03:00
Davidson Gomes
5239e431c2 feat: update server and monitor services
Updated server.module.ts and monitor.service.ts to improve service initialization and monitoring logic. Modified main.ts to integrate changes. This enhances the application's performance and reliability.
2024-06-12 19:48:56 -03:00
Davidson Gomes
53cc6132f5 Merge tag '1.8.1' into develop
v
2024-06-12 19:03:36 -03:00
Davidson Gomes
0bc1b78db9 Merge branch 'release/1.8.1' 2024-06-12 19:03:30 -03:00
Davidson Gomes
2a9412c81a chore: adjust socket configuration and update baileys version
Updated package.json to include the latest version of baileys for improved functionality. Modified whatsapp.baileys.service.ts to adjust socket configuration, enhancing the stability and performance of the service.
2024-06-12 19:02:36 -03:00
Davidson Gomes
f707cf4109 Merge tag '1.8.1' into develop
v
2024-06-09 14:22:20 -03:00
Davidson Gomes
410cfc8bcb Merge branch 'release/1.8.1' 2024-06-09 14:22:17 -03:00
Davidson Gomes
f1a3fd872f git actions v2 2024-06-09 14:20:48 -03:00
36 changed files with 1172 additions and 465 deletions

View File

@@ -0,0 +1,48 @@
name: Build Docker image
on:
push:
branches:
- main
jobs:
build_deploy:
name: Build and Deploy
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: atendai/evolution-api
tags: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View File

@@ -0,0 +1,48 @@
name: Build Docker image
on:
push:
branches:
- v2.0.0
jobs:
build_deploy:
name: Build and Deploy
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: atendai/evolution-api
tags: v2.0.0-alpha
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View File

@@ -1,3 +1,53 @@
# 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)
### Fixed
* 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
# 1.8.1 (2024-06-08 21:32)
### Feature

View File

@@ -1,6 +1,6 @@
{
"name": "evolution-api",
"version": "1.8.1",
"version": "1.8.6",
"description": "Rest api for communication with WhatsApp",
"main": "./dist/src/main.js",
"scripts": {
@@ -42,14 +42,14 @@
"homepage": "https://github.com/EvolutionAPI/evolution-api#readme",
"dependencies": {
"@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-sqs": "^3.569.0",
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@figuro/chatwoot-sdk": "^1.1.16",
"@hapi/boom": "^10.0.1",
"@sentry/node": "^7.59.2",
"amqplib": "^0.10.3",
"@aws-sdk/client-sqs": "^3.569.0",
"axios": "^1.6.5",
"@whiskeysockets/baileys": "6.7.4",
"baileys": "github:EvolutionAPI/Baileys",
"class-validator": "^0.14.1",
"compression": "^1.7.4",
"cors": "^2.8.5",
@@ -84,6 +84,7 @@
"redis": "^4.6.5",
"sharp": "^0.32.2",
"socket.io": "^4.7.1",
"socket.io-client": "^4.8.1",
"socks-proxy-agent": "^8.0.1",
"swagger-ui-express": "^5.0.0",
"uuid": "^9.0.0",

View File

@@ -86,6 +86,11 @@ export class ChatController {
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) {
logger.verbose('requested sendPresence from ' + instanceName + ' instance');
return await this.waMonitor.waInstances[instanceName].sendPresence(data);

View File

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

View File

@@ -1,4 +1,4 @@
import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys';
import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from 'baileys';
export class OnWhatsAppDto {
constructor(
@@ -84,7 +84,7 @@ class PrivacySetting {
status: WAPrivacyValue;
online: WAPrivacyOnlineValue;
last: WAPrivacyValue;
groupadd: WAPrivacyValue;
groupadd: any;
}
export class PrivacySettingDto {

View File

@@ -1,4 +1,4 @@
import { WAPresence } from '@whiskeysockets/baileys';
import { WAPresence } from 'baileys';
import { ProxyDto } from './proxy.dto';
@@ -21,6 +21,7 @@ export class InstanceDto {
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
wavoipToken?: string;
chatwoot_account_id?: string;
chatwoot_token?: string;
chatwoot_url?: string;

View File

@@ -1,4 +1,4 @@
import { proto, WAPresence } from '@whiskeysockets/baileys';
import { proto, WAPresence } from 'baileys';
export class Quoted {
key: proto.IMessageKey;

View File

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

View File

@@ -8,8 +8,8 @@ import ChatwootClient, {
inbox,
} from '@figuro/chatwoot-sdk';
import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request';
import { proto } from '@whiskeysockets/baileys';
import axios from 'axios';
import { proto } from 'baileys';
import FormData from 'form-data';
import { createReadStream, unlinkSync, writeFileSync } from 'fs';
import Jimp from 'jimp';
@@ -444,8 +444,7 @@ export class ChatwootService {
const searchableFields = this.getSearchableFields();
// eslint-disable-next-line prettier/prettier
if(contacts.length === 2 && this.getClientCwConfig().merge_brazil_contacts && query.startsWith('+55')){
if (contacts.length === 2 && this.getClientCwConfig().merge_brazil_contacts && query.startsWith('+55')) {
const contact = this.mergeBrazilianContacts(contacts);
if (contact) {
return contact;
@@ -736,7 +735,11 @@ export class ChatwootService {
}
this.logger.verbose('find inbox by name');
const findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().name_inbox);
let findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().name_inbox);
if (!findByName) {
findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().name_inbox.split('-cwId-')[0]);
}
if (!findByName) {
this.logger.warn('inbox not found');
@@ -1903,7 +1906,8 @@ export class ChatwootService {
let nameFile: string;
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) {
const parsedFile = path.parse(originalFilename);
if (parsedFile.name && parsedFile.ext) {

View File

@@ -1,5 +1,5 @@
import { inbox } from '@figuro/chatwoot-sdk';
import { proto } from '@whiskeysockets/baileys';
import { proto } from 'baileys';
import { InstanceDto } from '../../../../api/dto/instance.dto';
import { ChatwootRaw, ContactRaw, MessageRaw } from '../../../../api/models';

View File

@@ -6,45 +6,133 @@ import { Logger } from '../../../../config/logger.config';
const logger = new Logger('AMQP');
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 = () => {
return new Promise<void>((resolve, reject) => {
const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
amqp.connect(uri, (error, connection) => {
if (error) {
reject(error);
return;
}
connection.createChannel((channelError, channel) => {
if (channelError) {
reject(channelError);
return;
}
const exchangeName = 'evolution_exchange';
channel.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
amqpChannel = channel;
logger.info('AMQP initialized');
resolve();
});
});
connectToRabbitMQ(resolve, reject);
});
};
const connectToRabbitMQ = (resolve?: ResolveCallback, reject?: RejectCallback) => {
const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
amqp.connect(uri, (error, connection) => {
if (error) {
logger.error(`Failed to connect to RabbitMQ: ${error.message}`);
handleConnectionError(error, resolve, reject);
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) => {
if (channelError) {
logger.error(`Failed to create channel: ${channelError.message}`);
if (reject) {
reject(channelError);
}
return;
}
const exchangeName = 'evolution_exchange';
channel.assertExchange(exchangeName, 'topic', {
durable: true,
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;
logger.info('AMQP initialized');
if (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 => {
return amqpChannel;
};
export const initGlobalQueues = () => {
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) {
logger.warn('No events to initialize on AMQP');
@@ -54,9 +142,15 @@ export const initGlobalQueues = () => {
const eventKeys = Object.keys(events);
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 exchangeName = 'evolution_exchange';
@@ -70,6 +164,10 @@ export const initGlobalQueues = () => {
autoDelete: false,
arguments: {
'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[]) => {
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) => {
return `${event.replace(/_/g, '.').toLowerCase()}`;
@@ -100,6 +205,10 @@ export const initQueues = (instanceName: string, events: string[]) => {
autoDelete: false,
arguments: {
'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[]) => {
if (!events || !events.length) return;
if (!events || !events.length) {
return;
}
const channel = getAMQP();

View File

@@ -8,13 +8,17 @@ const logger = new Logger('Socket');
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) => {
if (configService.get<Websocket>('WEBSOCKET')?.ENABLED) {
io = new SocketIO(httpServer, {
cors: {
origin: cors,
origin,
methods,
credentials,
},
});

View File

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

View File

@@ -11,6 +11,7 @@ export class SettingsRaw {
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
wavoipToken?: string;
}
const settingsSchema = new Schema<SettingsRaw>({
@@ -22,6 +23,7 @@ const settingsSchema = new Schema<SettingsRaw>({
read_messages: { type: Boolean, required: true },
read_status: { 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');

View File

@@ -253,6 +253,23 @@ export class ChatRouter extends RouterBroker {
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) => {
logger.verbose('request received in sendPresence');
logger.verbose('request body: ');

View File

@@ -99,22 +99,28 @@ export class InstanceRouter extends RouterBroker {
return res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchInstances', false), ...guards, async (req, res) => {
logger.verbose('request received in fetchInstances');
logger.verbose('request body: ');
logger.verbose(req.body);
try {
logger.verbose('request received in fetchInstances');
logger.verbose('request body: ');
logger.verbose(req.body);
const key = req.get('apikey');
const key = req.get('apikey');
logger.verbose('request query: ');
logger.verbose(req.query);
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: null,
ClassRef: InstanceDto,
execute: (instance) => instanceController.fetchInstances(instance, key),
});
logger.verbose('request query: ');
logger.verbose(req.query);
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: null,
ClassRef: InstanceDto,
execute: (instance) => instanceController.fetchInstances(instance, key),
});
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) => {
logger.verbose('request received in setPresence');

View File

@@ -1,5 +1,5 @@
import { CacheEngine } from '../cache/cacheengine';
import { configService } from '../config/env.config';
import { configService, ProviderSession } from '../config/env.config';
import { eventEmitter } from '../config/event.config';
import { Logger } from '../config/logger.config';
import { dbserver } from '../libs/db.connect';
@@ -110,7 +110,12 @@ export const repository = new RepositoryBroker(
export const cache = new CacheService(new CacheEngine(configService, 'instance').getEngine());
const chatwootCache = new CacheService(new CacheEngine(configService, ChatwootService.name).getEngine());
const baileysCache = new CacheService(new CacheEngine(configService, 'baileys').getEngine());
const providerFiles = new ProviderFiles(configService);
let providerFiles: ProviderFiles = null;
if (configService.get<ProviderSession>('PROVIDER')?.ENABLED) {
providerFiles = new ProviderFiles(configService);
}
export const waMonitor = new WAMonitoringService(
eventEmitter,

View File

@@ -1,4 +1,4 @@
import { BufferJSON } from '@whiskeysockets/baileys';
import { BufferJSON } from 'baileys';
import { Logger } from '../../config/logger.config';
import { ICache } from '../abstract/abstract.cache';

View File

@@ -1,5 +1,5 @@
import { WASocket } from '@whiskeysockets/baileys';
import axios from 'axios';
import { WASocket } from 'baileys';
import { execSync } from 'child_process';
import { isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
@@ -181,6 +181,9 @@ export class ChannelStartupService {
this.localSettings.sync_full_history = data?.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');
}
@@ -194,8 +197,15 @@ export class ChannelStartupService {
this.logger.verbose(`Settings read_messages: ${data.read_messages}`);
this.logger.verbose(`Settings read_status: ${data.read_status}`);
this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`);
this.logger.verbose(`Settings wavoipToken: ${data.wavoipToken}`);
Object.assign(this.localSettings, data);
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() {
@@ -214,6 +224,7 @@ export class ChannelStartupService {
this.logger.verbose(`Settings read_messages: ${data.read_messages}`);
this.logger.verbose(`Settings read_status: ${data.read_status}`);
this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`);
this.logger.verbose(`Settings wavoipToken: ${data.wavoipToken}`);
return {
reject_call: data.reject_call,
msg_call: data.msg_call,
@@ -222,6 +233,7 @@ export class ChannelStartupService {
read_messages: data.read_messages,
read_status: data.read_status,
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 webhookLocal = this.localWebhook.events;
const websocketLocal = this.localWebsocket.events;
@@ -706,7 +756,7 @@ export class ChannelStartupService {
const tokenStore = await this.repository.auth.find(this.instanceName);
const instanceApikey = tokenStore?.apikey || 'Apikey not found';
if (rabbitmqEnabled) {
if (rabbitmqEnabled && integration.includes('rabbitmq')) {
const amqp = getAMQP();
if (this.localRabbitmq.enabled && amqp) {
if (Array.isArray(rabbitmqLocal) && rabbitmqLocal.includes(we)) {
@@ -721,7 +771,9 @@ export class ChannelStartupService {
autoDelete: false,
});
const queueName = `${this.instanceName}.${event}`;
const eventName = event.replace(/_/g, '.').toLowerCase();
const queueName = `${this.instanceName}.${eventName}`;
await amqp.assertQueue(queueName, {
durable: true,
@@ -731,7 +783,7 @@ export class ChannelStartupService {
},
});
await amqp.bindQueue(queueName, exchangeName, event);
await amqp.bindQueue(queueName, exchangeName, eventName);
const message = {
event,
@@ -746,7 +798,10 @@ export class ChannelStartupService {
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')) {
const logData = {
@@ -776,6 +831,7 @@ export class ChannelStartupService {
if (rabbitmqGlobal && rabbitmqEvents[we] && amqp) {
const exchangeName = 'evolution_exchange';
const prefixKey = this.configService.get<Rabbitmq>('RABBITMQ').PREFIX_KEY;
let retry = 0;
@@ -786,7 +842,9 @@ export class ChannelStartupService {
autoDelete: false,
});
const queueName = transformedWe;
const queueName = prefixKey
? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}`
: event.replace(/_/g, '.').toLowerCase();
await amqp.assertQueue(queueName, {
durable: true,
@@ -796,7 +854,7 @@ export class ChannelStartupService {
},
});
await amqp.bindQueue(queueName, exchangeName, event);
await amqp.bindQueue(queueName, exchangeName, queueName);
const message = {
event,
@@ -810,7 +868,11 @@ export class ChannelStartupService {
if (expose && 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')) {
const logData = {
@@ -839,7 +901,7 @@ export class ChannelStartupService {
}
}
if (this.localSqs.enabled) {
if (this.localSqs.enabled && integration.includes('sqs')) {
const sqs = getSQS();
if (sqs) {
@@ -909,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);
const io = getIO();
@@ -983,7 +1045,7 @@ export class ChannelStartupService {
const globalApiKey = this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;
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');
let baseURL: string;
@@ -1031,12 +1093,13 @@ export class ChannelStartupService {
postData['apikey'] = instanceApikey;
}
await httpService.post('', postData);
await this.retryWebhookRequest(httpService, postData, baseURL);
}
} catch (error) {
this.logger.error({
local: ChannelStartupService.name + '.sendDataWebhook-local',
message: error?.message,
message: 'Todas as tentativas de envio do webhook local falharam',
lastError: error?.message,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
@@ -1050,7 +1113,7 @@ export class ChannelStartupService {
}
}
if (webhookGlobal.GLOBAL?.ENABLED) {
if (webhookGlobal.GLOBAL?.ENABLED && integration.includes('webhook')) {
if (webhookGlobal.EVENTS[we]) {
this.logger.verbose('Sending data to webhook global');
const globalWebhook = this.configService.get<Webhook>('WEBHOOK').GLOBAL;
@@ -1102,12 +1165,13 @@ export class ChannelStartupService {
postData['apikey'] = globalApiKey;
}
await httpService.post('', postData);
await this.retryWebhookRequest(httpService, postData, globalURL, true);
}
} catch (error) {
this.logger.error({
local: ChannelStartupService.name + '.sendDataWebhook-global',
message: error?.message,
message: 'Todas as tentativas de envio do webhook global falharam',
lastError: error?.message,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
@@ -1281,4 +1345,36 @@ export class ChannelStartupService {
this.logger.verbose('Fetching chats');
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

@@ -1,5 +1,6 @@
import ffmpegPath from '@ffmpeg-installer/ffmpeg';
import { Boom } from '@hapi/boom';
import axios from 'axios';
import makeWASocket, {
AnyMessageContent,
BufferedEventData,
@@ -19,12 +20,12 @@ import makeWASocket, {
GroupMetadata,
isJidBroadcast,
isJidGroup,
isJidNewsletter,
isJidUser,
makeCacheableSignalKeyStore,
MessageUpsertType,
MiscMessageGenerationOptions,
ParticipantAction,
PHONENUMBER_MCC,
prepareWAMessageMedia,
proto,
useMultiFileAuthState,
@@ -35,16 +36,14 @@ import makeWASocket, {
WAMessageUpdate,
WAPresence,
WASocket,
} from '@whiskeysockets/baileys';
import { Label } from '@whiskeysockets/baileys/lib/Types/Label';
import { LabelAssociation } from '@whiskeysockets/baileys/lib/Types/LabelAssociation';
import axios from 'axios';
} from 'baileys';
import { Label } from 'baileys/lib/Types/Label';
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
import { exec } from 'child_process';
import { isBase64, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
// import ffmpeg from 'fluent-ffmpeg';
import fs, { existsSync, readFileSync } from 'fs';
import { parsePhoneNumber } from 'libphonenumber-js';
import Long from 'long';
import NodeCache from 'node-cache';
import { getMIMEType } from 'node-mime-types';
@@ -65,6 +64,7 @@ import {
Log,
ProviderSession,
QrCode,
Websocket,
} from '../../../config/env.config';
import { INSTANCE_DIR } from '../../../config/path.config';
import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../../exceptions';
@@ -131,6 +131,7 @@ import { waMonitor } from '../../server.module';
import { Events, MessageSubtype, TypeMediaMessage, wa } from '../../types/wa.types';
import { CacheService } from './../cache.service';
import { ChannelStartupService } from './../channel.service';
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
@@ -253,8 +254,8 @@ export class BaileysStartupService extends ChannelStartupService {
this.logger.verbose('Getting profile status');
const status = await this.client.fetchStatus(this.instance.wuid);
this.logger.verbose(`Profile status: ${status.status}`);
return status.status;
this.logger.verbose(`Profile status: ${status[0]?.status}`);
return status[0]?.status;
}
public get profilePictureUrl() {
@@ -380,12 +381,6 @@ export class BaileysStartupService extends ChannelStartupService {
state: connection,
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') {
@@ -418,6 +413,15 @@ export class BaileysStartupService extends ChannelStartupService {
this.client?.ws?.close();
this.client.end(new Error('Close connection'));
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,
});
}
}
@@ -447,13 +451,34 @@ export class BaileysStartupService extends ChannelStartupService {
{
instance: this.instance.name,
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 (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,
});
}
}
@@ -517,6 +542,165 @@ export class BaileysStartupService extends ChannelStartupService {
return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name));
}
private async createClient(number?: string, mobile?: boolean): Promise<WASocket> {
this.instance.authState = await this.defineAuthState();
if (!mobile) {
this.mobile = false;
} else {
this.mobile = mobile;
}
const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE');
let browserOptions = {};
if (number || this.phoneNumber) {
this.phoneNumber = number;
this.logger.info(`Phone number: ${number}`);
} else {
const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()];
browserOptions = { browser };
this.logger.info(`Browser: ${browser}`);
}
let version;
let log;
if (session.VERSION) {
version = session.VERSION.split(',');
log = `Baileys version env: ${version}`;
} else {
const baileysVersion = await fetchLatestBaileysVersion();
version = baileysVersion.version;
log = `Baileys version: ${version}`;
}
this.logger.info(log);
let options;
if (this.localProxy.enabled) {
this.logger.info('Proxy enabled: ' + this.localProxy.proxy?.host);
if (this.localProxy?.proxy?.host?.includes('proxyscrape')) {
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),
fetchAgent: makeProxyAgent(proxyUrl),
};
} catch (error) {
this.localProxy.enabled = false;
}
} else {
options = {
agent: makeProxyAgent(this.localProxy.proxy),
fetchAgent: makeProxyAgent(this.localProxy.proxy),
};
}
}
const socketConfig: UserFacingSocketConfig = {
...options,
auth: {
creds: this.instance.authState.state.creds,
keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any),
},
logger: P({ level: this.logBaileys }),
printQRInTerminal: false,
mobile,
...browserOptions,
version,
markOnlineOnConnect: this.localSettings.always_online,
retryRequestDelayMs: 350,
maxMsgRetryCount: 4,
fireInitQueries: true,
connectTimeoutMs: 20_000,
keepAliveIntervalMs: 30_000,
qrTimeout: 45_000,
defaultQueryTimeoutMs: undefined,
emitOwnEvents: false,
shouldIgnoreJid: (jid) => {
const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid);
const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid);
const isNewsletter = isJidNewsletter(jid);
return isGroupJid || isBroadcast || isNewsletter;
},
msgRetryCounterCache: this.msgRetryCounterCache,
getMessage: async (key) => (await this.getMessage(key)) as Promise<proto.IMessage>,
generateHighQualityLinkPreview: true,
syncFullHistory: this.localSettings.sync_full_history,
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => {
return this.historySyncNotification(msg);
},
userDevicesCache: this.userDevicesCache,
transactionOpts: { maxCommitRetries: 5, delayBetweenTriesMs: 2500 },
patchMessageBeforeSending(message) {
if (
message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST
) {
message = JSON.parse(JSON.stringify(message));
message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
}
if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) {
message = JSON.parse(JSON.stringify(message));
message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
}
return message;
},
};
this.endSession = false;
this.logger.verbose('Creating socket');
this.client = makeWASocket(socketConfig);
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.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;
return this.client;
}
public async connectToWhatsapp(number?: string, mobile?: boolean): Promise<WASocket> {
this.logger.verbose('Connecting to whatsapp');
try {
@@ -530,126 +714,7 @@ export class BaileysStartupService extends ChannelStartupService {
this.loadProxy();
this.loadChamaai();
this.instance.authState = await this.defineAuthState();
if (!mobile) {
this.mobile = false;
} else {
this.mobile = mobile;
}
const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE');
const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()];
this.logger.verbose('Browser: ' + JSON.stringify(browser));
let version;
let log;
if (session.VERSION) {
version = session.VERSION.split(',');
log = `Baileys version env: ${version}`;
} else {
const baileysVersion = await fetchLatestBaileysVersion();
version = baileysVersion.version;
log = `Baileys version: ${version}`;
}
this.logger.info(log);
let options;
if (this.localProxy.enabled) {
this.logger.info('Proxy enabled: ' + this.localProxy.proxy?.host);
if (this.localProxy?.proxy?.host?.includes('proxyscrape')) {
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),
fetchAgent: makeProxyAgent(proxyUrl),
};
} catch (error) {
this.localProxy.enabled = false;
}
} else {
options = {
agent: makeProxyAgent(this.localProxy.proxy),
fetchAgent: makeProxyAgent(this.localProxy.proxy),
};
}
}
const socketConfig: UserFacingSocketConfig = {
...options,
auth: {
creds: this.instance.authState.state.creds,
keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any),
},
logger: P({ level: this.logBaileys }),
printQRInTerminal: false,
mobile,
browser: number ? ['Chrome (Linux)', session.NAME, release()] : browser,
version,
markOnlineOnConnect: this.localSettings.always_online,
retryRequestDelayMs: 10,
connectTimeoutMs: 60_000,
qrTimeout: 40_000,
defaultQueryTimeoutMs: undefined,
emitOwnEvents: false,
shouldIgnoreJid: (jid) => {
const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid);
const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid);
return isGroupJid || isBroadcast;
},
msgRetryCounterCache: this.msgRetryCounterCache,
getMessage: async (key) => (await this.getMessage(key)) as Promise<proto.IMessage>,
generateHighQualityLinkPreview: true,
syncFullHistory: this.localSettings.sync_full_history,
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => {
return this.historySyncNotification(msg);
},
userDevicesCache: this.userDevicesCache,
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 },
patchMessageBeforeSending(message) {
if (
message.deviceSentMessage?.message?.listMessage?.listType ===
proto.Message.ListMessage.ListType.PRODUCT_LIST
) {
message = JSON.parse(JSON.stringify(message));
message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
}
if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) {
message = JSON.parse(JSON.stringify(message));
message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
}
return message;
},
};
this.endSession = false;
this.logger.verbose('Creating socket');
this.client = makeWASocket(socketConfig);
this.logger.verbose('Socket created');
this.eventHandler();
this.logger.verbose('Socket event handler initialized');
this.phoneNumber = number;
return this.client;
return await this.createClient(number, mobile);
} catch (error) {
this.logger.error(error);
throw new InternalServerErrorException(error?.toString());
@@ -657,165 +722,56 @@ export class BaileysStartupService extends ChannelStartupService {
}
private async sendMobileCode() {
const { registration } = this.client.authState.creds || null;
let phoneNumber = registration.phoneNumber || this.phoneNumber;
if (!phoneNumber.startsWith('+')) {
phoneNumber = '+' + phoneNumber;
}
if (!phoneNumber) {
this.logger.error('Phone number not found');
return;
}
const parsedPhoneNumber = parsePhoneNumber(phoneNumber);
if (!parsedPhoneNumber?.isValid()) {
this.logger.error('Phone number invalid');
return;
}
registration.phoneNumber = parsedPhoneNumber.format('E.164');
registration.phoneNumberCountryCode = parsedPhoneNumber.countryCallingCode;
registration.phoneNumberNationalNumber = parsedPhoneNumber.nationalNumber;
const mcc = await PHONENUMBER_MCC[parsedPhoneNumber.countryCallingCode];
if (!mcc) {
this.logger.error('MCC not found');
return;
}
registration.phoneNumberMobileCountryCode = mcc;
registration.method = 'sms';
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);
}
// const { registration } = this.client.authState.creds || null;
// let phoneNumber = registration.phoneNumber || this.phoneNumber;
// if (!phoneNumber.startsWith('+')) {
// phoneNumber = '+' + phoneNumber;
// }
// if (!phoneNumber) {
// this.logger.error('Phone number not found');
// return;
// }
// const parsedPhoneNumber = parsePhoneNumber(phoneNumber);
// if (!parsedPhoneNumber?.isValid()) {
// this.logger.error('Phone number invalid');
// return;
// }
// registration.phoneNumber = parsedPhoneNumber.format('E.164');
// registration.phoneNumberCountryCode = parsedPhoneNumber.countryCallingCode;
// registration.phoneNumberNationalNumber = parsedPhoneNumber.nationalNumber;
// const mcc = await PHONENUMBER_MCC[parsedPhoneNumber.countryCallingCode];
// if (!mcc) {
// this.logger.error('MCC not found');
// return;
// }
// registration.phoneNumberMobileCountryCode = mcc;
// registration.method = 'sms';
// 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) {
await this.client
.register(code.replace(/["']/g, '').trim().toLowerCase())
.then(async () => {
this.logger.verbose('Registration code received successfully');
})
.catch((error) => {
this.logger.error(error);
});
console.log(code);
// await this.client
// .register(code.replace(/["']/g, '').trim().toLowerCase())
// .then(async () => {
// this.logger.verbose('Registration code received successfully');
// })
// .catch((error) => {
// this.logger.error(error);
// });
}
public async reloadConnection(): Promise<WASocket> {
try {
this.instance.authState = await this.defineAuthState();
const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE');
const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()];
let version;
let log;
if (session.VERSION) {
version = session.VERSION.split(',');
log = `Baileys version env: ${version}`;
} else {
const baileysVersion = await fetchLatestBaileysVersion();
version = baileysVersion.version;
log = `Baileys version: ${version}`;
}
this.logger.info(log);
let options;
if (this.localProxy.enabled) {
this.logger.info('Proxy enabled: ' + this.localProxy.proxy?.host);
if (this.localProxy?.proxy?.host?.includes('proxyscrape')) {
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),
fetchAgent: makeProxyAgent(proxyUrl),
};
} catch (error) {
this.localProxy.enabled = false;
}
} else {
options = {
agent: makeProxyAgent(this.localProxy.proxy),
fetchAgent: makeProxyAgent(this.localProxy.proxy),
};
}
}
const socketConfig: UserFacingSocketConfig = {
...options,
auth: {
creds: this.instance.authState.state.creds,
keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any),
},
logger: P({ level: this.logBaileys }),
printQRInTerminal: false,
browser: this.phoneNumber ? ['Chrome (Linux)', session.NAME, release()] : browser,
version,
markOnlineOnConnect: this.localSettings.always_online,
retryRequestDelayMs: 10,
connectTimeoutMs: 60_000,
qrTimeout: 40_000,
defaultQueryTimeoutMs: undefined,
emitOwnEvents: false,
shouldIgnoreJid: (jid) => {
const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid);
const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid);
return isGroupJid || isBroadcast;
},
msgRetryCounterCache: this.msgRetryCounterCache,
getMessage: async (key) => (await this.getMessage(key)) as Promise<proto.IMessage>,
generateHighQualityLinkPreview: true,
syncFullHistory: this.localSettings.sync_full_history,
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => {
return this.historySyncNotification(msg);
},
userDevicesCache: this.userDevicesCache,
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 },
patchMessageBeforeSending(message) {
if (
message.deviceSentMessage?.message?.listMessage?.listType ===
proto.Message.ListMessage.ListType.PRODUCT_LIST
) {
message = JSON.parse(JSON.stringify(message));
message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
}
if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) {
message = JSON.parse(JSON.stringify(message));
message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
}
return message;
},
};
this.client = makeWASocket(socketConfig);
return this.client;
return await this.createClient(this.phoneNumber, this.mobile);
} catch (error) {
this.logger.error(error);
throw new InternalServerErrorException(error?.toString());
@@ -977,7 +933,7 @@ export class BaileysStartupService extends ChannelStartupService {
chats: Chat[];
contacts: Contact[];
messages: proto.IWebMessageInfo[];
isLatest: boolean;
isLatest?: boolean;
},
database: Database,
) => {
@@ -1053,7 +1009,7 @@ export class BaileysStartupService extends ChannelStartupService {
m.messageTimestamp = m.messageTimestamp?.toNumber();
}
if (m.messageTimestamp <= timestampLimitToImport) {
if ((m.messageTimestamp as number) <= timestampLimitToImport) {
continue;
}
@@ -1097,7 +1053,7 @@ export class BaileysStartupService extends ChannelStartupService {
await this.contactHandle['contacts.upsert'](
contacts
.filter((c) => !!c.notify ?? !!c.name)
.filter((c) => !!c.notify || !!c.name)
.map((c) => ({
id: c.id,
name: c.name ?? c.notify,
@@ -1178,11 +1134,17 @@ export class BaileysStartupService extends ChannelStartupService {
received?.message?.videoMessage ||
received?.message?.stickerMessage ||
received?.message?.documentMessage ||
received?.message?.documentWithCaptionMessage ||
received?.message?.audioMessage;
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(
{ key: received.key, message: received?.message },
'buffer',
@@ -1729,7 +1691,7 @@ export class BaileysStartupService extends ChannelStartupService {
this.logger.verbose('Getting status');
return {
wuid: jid,
status: (await this.client.fetchStatus(jid))?.status,
status: (await this.client.fetchStatus(jid))[0]?.status,
};
} catch (error) {
this.logger.verbose('Status not found');
@@ -1939,11 +1901,9 @@ export class BaileysStartupService extends ChannelStartupService {
} as unknown as AnyMessageContent,
{
...option,
cachedGroupMetadata:
!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED
? null
: this.getGroupMetadataCache,
useCachedGroupMetadata:
!!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED,
} as unknown as MiscMessageGenerationOptions,
);
}
@@ -1959,11 +1919,9 @@ export class BaileysStartupService extends ChannelStartupService {
} as unknown as AnyMessageContent,
{
...option,
cachedGroupMetadata:
!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED
? null
: this.getGroupMetadataCache,
useCachedGroupMetadata:
!!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED,
} as unknown as MiscMessageGenerationOptions,
);
}
@@ -1981,11 +1939,9 @@ export class BaileysStartupService extends ChannelStartupService {
},
{
...option,
cachedGroupMetadata:
!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED
? null
: this.getGroupMetadataCache,
useCachedGroupMetadata:
!!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED,
} as unknown as MiscMessageGenerationOptions,
);
}
@@ -2009,11 +1965,9 @@ export class BaileysStartupService extends ChannelStartupService {
message as unknown as AnyMessageContent,
{
...option,
cachedGroupMetadata:
!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED
? null
: this.getGroupMetadataCache,
useCachedGroupMetadata:
!!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED,
} as unknown as MiscMessageGenerationOptions,
);
})();
@@ -2031,6 +1985,32 @@ export class BaileysStartupService extends ChannelStartupService {
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.verbose('Sending data to webhook in event SEND_MESSAGE');
@@ -2677,7 +2657,7 @@ export class BaileysStartupService extends ChannelStartupService {
const group = await this.findGroup({ groupJid: jid }, 'inner');
if (!group) {
new OnWhatsAppDto(jid, false, number);
return new OnWhatsAppDto(jid, false, number);
}
return new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject);
@@ -3377,6 +3357,10 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async fetchAllGroups(getParticipants: GetParticipant) {
if (this.localSettings.groups_ignore === true) {
return;
}
this.logger.verbose('Fetching all groups');
try {
const fetch = Object.values(await this.client.groupFetchAllParticipating());

View File

@@ -743,6 +743,7 @@ export class BusinessStartupService extends ChannelStartupService {
[message['type']]: message['id'],
preview_url: linkPreview,
caption: message['caption'],
filename: message['fileName'],
},
};
quoted ? (content.context = { message_id: quoted.id }) : content;
@@ -1212,7 +1213,7 @@ export class BusinessStartupService extends ChannelStartupService {
try {
const msg = data.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];
this.logger.verbose('Media message downloaded');

View File

@@ -323,7 +323,7 @@ export class WAMonitoringService {
this.logger.verbose('Loading instances');
try {
if (this.providerSession.ENABLED) {
if (this.providerSession?.ENABLED) {
await this.loadInstancesFromProvider();
} else if (this.redis.REDIS.ENABLED && this.redis.REDIS.SAVE_INSTANCES) {
await this.loadInstancesFromRedis();

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { AuthenticationState, WAConnectionState } from '@whiskeysockets/baileys';
import { AuthenticationState, WAConnectionState } from 'baileys';
export enum Events {
APPLICATION_STARTUP = 'application.startup',
@@ -83,6 +83,7 @@ export declare namespace wa {
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
wavoipToken?: string;
};
export type LocalWebsocket = {

View File

@@ -1,4 +1,4 @@
import { BufferJSON } from '@whiskeysockets/baileys';
import { BufferJSON } from 'baileys';
import { RedisClientType } from 'redis';
import { ICache } from '../api/abstract/abstract.cache';

View File

@@ -104,7 +104,11 @@ export type Rabbitmq = {
ENABLED: boolean;
URI: string;
EXCHANGE_NAME: string;
PREFIX_KEY?: string;
GLOBAL_ENABLED: boolean;
MESSAGE_TTL: number;
MAX_LENGTH: number;
MAX_LENGTH_BYTES: number;
EVENTS: EventsRabbitmq;
};
@@ -323,7 +327,11 @@ export class ConfigService {
ENABLED: process.env?.RABBITMQ_ENABLED === 'true',
GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true',
EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange',
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY || '',
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: {
APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true',
INSTANCE_CREATE: process.env?.RABBITMQ_EVENTS_INSTANCE_CREATE === 'true',
@@ -366,9 +374,9 @@ export class ConfigService {
GLOBAL_EVENTS: process.env?.WEBSOCKET_GLOBAL_EVENTS === 'true',
},
WA_BUSINESS: {
TOKEN_WEBHOOK: process.env.WA_BUSINESS_TOKEN_WEBHOOK || '',
URL: process.env.WA_BUSINESS_URL || '',
VERSION: process.env.WA_BUSINESS_VERSION || '',
TOKEN_WEBHOOK: process.env.WA_BUSINESS_TOKEN_WEBHOOK || 'evolution',
URL: process.env.WA_BUSINESS_URL || 'https://graph.facebook.com',
VERSION: process.env.WA_BUSINESS_VERSION || 'v19.0',
LANGUAGE: process.env.WA_BUSINESS_LANGUAGE || 'en',
},
LOG: {

View File

@@ -89,7 +89,14 @@ RABBITMQ:
ENABLED: false
URI: "amqp://guest:guest@localhost:5672"
EXCHANGE_NAME: evolution_exchange
PREFIX_KEY: evolution
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:
APPLICATION_STARTUP: false
INSTANCE_CREATE: false

View File

@@ -25,7 +25,7 @@ info:
</font>
[![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/26869335-5546d063-156b-4529-915f-909dd628c090?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D26869335-5546d063-156b-4529-915f-909dd628c090%26entityType%3Dcollection%26workspaceId%3D339a4ee7-378b-45c9-b5b8-fd2c0a9c2442)
version: 1.8.0
version: 1.8.2
contact:
name: DavidsonGomes
email: contato@agenciadgcode.com

View File

@@ -12,7 +12,7 @@ import { initIO } from './api/integrations/websocket/libs/socket.server';
import { ProviderFiles } from './api/provider/sessions';
import { HttpStatus, router } from './api/routes/index.router';
import { waMonitor } from './api/server.module';
import { Auth, configService, Cors, HttpServer, Rabbitmq, Sqs, Webhook } from './config/env.config';
import { Auth, configService, Cors, HttpServer, ProviderSession, Rabbitmq, Sqs, Webhook } from './config/env.config';
import { onUnexpectedError } from './config/error.config';
import { Logger } from './config/logger.config';
import { ROOT_DIR } from './config/path.config';
@@ -27,9 +27,13 @@ async function bootstrap() {
const logger = new Logger('SERVER');
const app = express();
const providerFiles = new ProviderFiles(configService);
await providerFiles.onModuleInit();
logger.info('Provider:Files - ON');
let providerFiles: ProviderFiles = null;
if (configService.get<ProviderSession>('PROVIDER')?.ENABLED) {
providerFiles = new ProviderFiles(configService);
await providerFiles.onModuleInit();
logger.info('Provider:Files - ON');
}
app.use(
cors({

View File

@@ -1,29 +1,55 @@
import {
AuthenticationCreds,
AuthenticationState,
BufferJSON,
initAuthCreds,
proto,
SignalDataTypeMap,
} from '@whiskeysockets/baileys';
import { AuthenticationState, BufferJSON, initAuthCreds, WAProto as proto } from 'baileys';
import fs from 'fs/promises';
import path from 'path';
import { configService, Database } from '../config/env.config';
import { Logger } from '../config/logger.config';
import { INSTANCE_DIR } from '../config/path.config';
import { dbserver } from '../libs/db.connect';
const fixFileName = (file) => {
if (!file) {
return undefined;
}
const replacedSlash = file.replace(/\//g, '__');
const replacedColon = replacedSlash.replace(/:/g, '-');
return replacedColon;
};
async function fileExists(file) {
try {
const stat = await fs.stat(file);
if (stat.isFile()) return true;
} catch (error) {
return;
}
}
export async function useMultiFileAuthStateDb(
coll: string,
): Promise<{ state: AuthenticationState; saveCreds: () => Promise<void> }> {
const logger = new Logger(useMultiFileAuthStateDb.name);
const client = dbserver.getClient();
const logger = new Logger(useMultiFileAuthStateDb.name);
const collection = client
.db(configService.get<Database>('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances')
.collection(coll);
const writeData = async (data: any, key: string): Promise<any> => {
const sessionId = coll;
const localFolder = path.join(INSTANCE_DIR, sessionId);
const localFile = (key: string) => path.join(localFolder, fixFileName(key) + '.json');
await fs.mkdir(localFolder, { recursive: true });
async function writeData(data: any, key: string): Promise<any> {
try {
const dataString = JSON.stringify(data, BufferJSON.replacer);
if (key != 'creds') {
await fs.writeFile(localFile(key), dataString);
return;
}
await client.connect();
let msgParsed = JSON.parse(JSON.stringify(data, BufferJSON.replacer));
if (Array.isArray(msgParsed)) {
@@ -37,42 +63,59 @@ export async function useMultiFileAuthStateDb(
});
} catch (error) {
logger.error(error);
return;
}
};
}
const readData = async (key: string): Promise<any> => {
async function readData(key: string): Promise<any> {
try {
await client.connect();
let data = (await collection.findOne({ _id: key })) as any;
if (data?.content_array) {
data = data.content_array;
if (key != 'creds') {
if (!(await fileExists(localFile(key)))) return null;
const rawData = await fs.readFile(localFile(key), { encoding: 'utf-8' });
const parsedData = JSON.parse(rawData, BufferJSON.reviver);
return parsedData;
} else {
await client.connect();
let data = (await collection.findOne({ _id: key })) as any;
if (data?.content_array) {
data = data.content_array;
}
const creds = JSON.stringify(data);
return JSON.parse(creds, BufferJSON.reviver);
}
const creds = JSON.stringify(data);
return JSON.parse(creds, BufferJSON.reviver);
} catch (error) {
logger.error(error);
return null;
}
};
}
const removeData = async (key: string) => {
async function removeData(key: string): Promise<any> {
try {
await client.connect();
return await collection.deleteOne({ _id: key });
if (key != 'creds') {
await fs.unlink(localFile(key));
} else {
await client.connect();
return await collection.deleteOne({ _id: key });
}
} catch (error) {
logger.error(error);
return;
}
};
}
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
let creds = await readData('creds');
if (!creds) {
creds = initAuthCreds();
await writeData(creds, 'creds');
}
return {
state: {
creds,
keys: {
get: async (type, ids: string[]) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: { [_: string]: SignalDataTypeMap[type] } = {};
get: async (type, ids) => {
const data = {};
await Promise.all(
ids.map(async (id) => {
let value = await readData(`${type}-${id}`);
@@ -83,25 +126,24 @@ export async function useMultiFileAuthStateDb(
data[id] = value;
}),
);
return data;
},
set: async (data: any) => {
const tasks: Promise<void>[] = [];
set: async (data) => {
const tasks = [];
for (const category in data) {
for (const id in data[category]) {
const value = data[category][id];
const key = `${category}-${id}`;
tasks.push(value ? writeData(value, key) : removeData(key));
}
}
await Promise.all(tasks);
},
},
},
saveCreds: async () => {
return await writeData(creds, 'creds');
saveCreds: () => {
return writeData(creds, 'creds');
},
};
}

View File

@@ -34,14 +34,7 @@
* └──────────────────────────────────────────────────────────────────────────────┘
*/
import {
AuthenticationCreds,
AuthenticationState,
BufferJSON,
initAuthCreds,
proto,
SignalDataTypeMap,
} from '@whiskeysockets/baileys';
import { AuthenticationCreds, AuthenticationState, BufferJSON, initAuthCreds, proto, SignalDataTypeMap } from 'baileys';
import { isNotEmpty } from 'class-validator';
import { ProviderFiles } from '../api/provider/sessions';

View File

@@ -1,10 +1,4 @@
import {
AuthenticationCreds,
AuthenticationState,
initAuthCreds,
proto,
SignalDataTypeMap,
} from '@whiskeysockets/baileys';
import { AuthenticationCreds, AuthenticationState, initAuthCreds, proto, SignalDataTypeMap } from 'baileys';
import { CacheService } from '../api/services/cache.service';
import { Logger } from '../config/logger.config';

View File

@@ -1002,9 +1002,26 @@ export const settingsSchema: JSONSchema7 = {
read_messages: { type: 'boolean', enum: [true, false] },
read_status: { 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'],
...isNotEmpty('reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status', 'sync_full_history'),
required: [
'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 = {