Merge branch 'release/1.1.0'

This commit is contained in:
Davidson Gomes 2023-06-21 11:18:25 -03:00
commit e05690af4c
47 changed files with 1804 additions and 910 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.gitignore vendored
View File

@ -11,6 +11,9 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
/store/*
/docker-compose-data
# Package # Package
/yarn.lock /yarn.lock
/package-lock.json /package-lock.json

View File

@ -1,3 +1,32 @@
# 1.1.0 (2023-06-21 11:17)
### Features
* Improved fetch instances endpoint, now it also fetch other instances even if they are not connected
* Added conversion of audios for sending recorded audio, now it is possible to send mp3 audios and not just ogg
* Route to fetch all groups that the connection is part of
* Route to fetch all privacy settings
* Route to update the privacy settings
* Route to update group subject
* Route to update group description
* Route to accept invite code
* Added configuration of events by webhook of instances
* Now the api key can be exposed in fetch instances if the EXPOSE_IN_FETCH_INSTANCES variable is set to true
* Added option to generate qrcode as soon as the instance is created
* The created instance token can now also be optionally defined manually in the creation endpoint
* Route to send Sticker
### Fixed
* Adjust dockerfile variables
* tweaks in docker-compose to pass variables
* Adjust the route getProfileBusiness to fetchProfileBusiness
* fix error after logout and try to get status or to connect again
* fix sending narrated audio on whatsapp android and ios
* fixed the problem of not disabling the global webhook by the variable
* Adjustment in the recording of temporary files and periodic cleaning
* Fix for container mode also work only with files
# 1.0.9 (2023-06-10) # 1.0.9 (2023-06-10)
### Fixed ### Fixed

View File

@ -12,11 +12,17 @@ LOG_COLOR=true
DEL_INSTANCE=5 DEL_INSTANCE=5
# Temporary data storage # Temporary data storage
STORE_CLEANING_INTERVAL=7200 # seconds ===2h STORE_MESSAGES=true
STORE_MESSAGE=true STORE_MESSAGE_UP=true
STORE_CONTACTS=false STORE_CONTACTS=false
STORE_CHATS=false STORE_CHATS=false
CLEAN_STORE_CLEANING_INTERVAL=7200 # seconds ===2h
CLEAN_STORE_MESSAGES=true
CLEAN_STORE_MESSAGE_UP=true
CLEAN_STORE_CONTACTS=false
CLEAN_STORE_CHATS=false
# Permanent data storage # Permanent data storage
DATABASE_ENABLED=false DATABASE_ENABLED=false
DATABASE_CONNECTION_URI='<uri>' DATABASE_CONNECTION_URI='<uri>'
@ -70,6 +76,7 @@ AUTHENTICATION_TYPE='jwt' # or 'apikey'
## Define a global apikey to access all instances. ## Define a global apikey to access all instances.
### OBS: This key must be inserted in the request header to create an instance. ### OBS: This key must be inserted in the request header to create an instance.
AUTHENTICATION_API_KEY='t8OOEeISKzpmc3jjcMqBWYSaJsafdefer' AUTHENTICATION_API_KEY='t8OOEeISKzpmc3jjcMqBWYSaJsafdefer'
AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true
## Set the secret key to encrypt and decrypt your token and its expiration time ## Set the secret key to encrypt and decrypt your token and its expiration time
AUTHENTICATION_JWT_EXPIRIN_IN=3600 # seconds - 3600s ===1h | zero (0) - never expires AUTHENTICATION_JWT_EXPIRIN_IN=3600 # seconds - 3600s ===1h | zero (0) - never expires
AUTHENTICATION_JWT_SECRET='L0YWtjb2w554WFqPG' AUTHENTICATION_JWT_SECRET='L0YWtjb2w554WFqPG'

View File

@ -9,6 +9,9 @@ COPY ./package.json .
ENV DOCKER_ENV=true ENV DOCKER_ENV=true
ENV SERVER_TYPE="http"
ENV SERVER_PORT=8080
ENV CORS_ORIGIN="*" ENV CORS_ORIGIN="*"
ENV CORS_METHODS="POST,GET,PUT,DELETE" ENV CORS_METHODS="POST,GET,PUT,DELETE"
ENV CORS_CREDENTIALS=true ENV CORS_CREDENTIALS=true
@ -16,12 +19,18 @@ ENV CORS_CREDENTIALS=true
ENV LOG_LEVEL="ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK" ENV LOG_LEVEL="ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK"
ENV LOG_COLOR=true ENV LOG_COLOR=true
ENV DEL_INSTANCE=false ENV DEL_INSTANCE=$DEL_INSTANCE
ENV STORE_CLEANING_INTERVAL=7200 ENV STORE_MESSAGES=$STORE_MESSAGE
ENV STORE_MESSAGE=true ENV STORE_MESSAGE_UP=$STORE_MESSAGE_UP
ENV STORE_CONTACTS=true ENV STORE_CONTACTS=$STORE_CONTACTS
ENV STORE_CHATS=true ENV STORE_CHATS=$STORE_CHATS
ENV CLEAN_STORE_CLEANING_INTERVAL=$CLEAN_STORE_CLEANING_INTERVAL
ENV CLEAN_STORE_MESSAGES=$CLEAN_STORE_MESSAGE
ENV CLEAN_STORE_MESSAGE_UP=$CLEAN_STORE_MESSAGE_UP
ENV CLEAN_STORE_CONTACTS=$CLEAN_STORE_CONTACTS
ENV CLEAN_STORE_CHATS=$CLEAN_STORE_CHATS
ENV DATABASE_ENABLED=$DATABASE_ENABLED ENV DATABASE_ENABLED=$DATABASE_ENABLED
ENV DATABASE_CONNECTION_URI=$DATABASE_CONNECTION_URI ENV DATABASE_CONNECTION_URI=$DATABASE_CONNECTION_URI
@ -37,7 +46,7 @@ ENV REDIS_ENABLED=$REDIS_ENABLED
ENV REDIS_URI=$REDIS_URI ENV REDIS_URI=$REDIS_URI
ENV WEBHOOK_GLOBAL_URL=$WEBHOOK_GLOBAL_URL ENV WEBHOOK_GLOBAL_URL=$WEBHOOK_GLOBAL_URL
ENV WEBHOOK_GLOBAL_ENABLED=true ENV WEBHOOK_GLOBAL_ENABLED=$WEBHOOK_GLOBAL_ENABLED
ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=$WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=$WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS
ENV WEBHOOK_EVENTS_STATUS_INSTANCE=$WEBHOOK_EVENTS_STATUS_INSTANCE ENV WEBHOOK_EVENTS_STATUS_INSTANCE=$WEBHOOK_EVENTS_STATUS_INSTANCE
@ -59,19 +68,20 @@ ENV WEBHOOK_EVENTS_GROUPS_UPSERT=$WEBHOOK_EVENTS_GROUPS_UPSERT
ENV WEBHOOK_EVENTS_GROUPS_UPDATE=$WEBHOOK_EVENTS_GROUPS_UPDATE ENV WEBHOOK_EVENTS_GROUPS_UPDATE=$WEBHOOK_EVENTS_GROUPS_UPDATE
ENV WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=$WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE ENV WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=$WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE
ENV WEBHOOK_EVENTS_NEW_JWT_TOKEN=true ENV WEBHOOK_EVENTS_NEW_JWT_TOKEN=$WEBHOOK_EVENTS_NEW_JWT_TOKEN
ENV CONFIG_SESSION_PHONE_CLIENT="Evolution API" ENV CONFIG_SESSION_PHONE_CLIENT=$CONFIG_SESSION_PHONE_CLIENT
ENV CONFIG_SESSION_PHONE_NAME="Chrome" ENV CONFIG_SESSION_PHONE_NAME="Chrome"
ENV QRCODE_LIMIT=30 ENV QRCODE_LIMIT=$QRCODE_LIMIT
ENV AUTHENTICATION_TYPE="apikey" ENV AUTHENTICATION_TYPE=$AUTHENTICATION_TYPE
ENV AUTHENTICATION_API_KEY=$AUTHENTICATION_API_KEY ENV AUTHENTICATION_API_KEY=$AUTHENTICATION_API_KEY
ENV AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=$AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES
ENV AUTHENTICATION_JWT_EXPIRIN_IN=0 ENV AUTHENTICATION_JWT_EXPIRIN_IN=$AUTHENTICATION_JWT_EXPIRIN_IN
ENV AUTHENTICATION_JWT_SECRET="L0YWtjb2w554WFqPG" ENV AUTHENTICATION_JWT_SECRET="L=0YWt]b2w[WF>#>:&E`"
ENV AUTHENTICATION_INSTANCE_NAME=$AUTHENTICATION_INSTANCE_NAME ENV AUTHENTICATION_INSTANCE_NAME=$AUTHENTICATION_INSTANCE_NAME
ENV AUTHENTICATION_INSTANCE_WEBHOOK_URL=$AUTHENTICATION_INSTANCE_WEBHOOK_URL ENV AUTHENTICATION_INSTANCE_WEBHOOK_URL=$AUTHENTICATION_INSTANCE_WEBHOOK_URL

View File

@ -1,22 +1,19 @@
<h1 align="center">Evolution Api</h1> <h1 align="center">Evolution Api</h1>
<!--
</br>
<hr style="height: 5px;background: #007500;margin: 20px 0;box-shadow: 0px 3px 5px 0px rgb(204 204 204);">-->
<!-- <div align="center"> --> <div align="center">
<!-- [![Telegram Group](https://img.shields.io/badge/Group-Telegram-%2333C1FF)](#) <!-- [![Telegram Group](https://img.shields.io/badge/Group-Telegram-%2333C1FF)](#)-->
[![Whatsapp Group](https://img.shields.io/badge/Group-WhatsApp-%2322BC18)](#) <!-- [![Whatsapp Group](https://img.shields.io/badge/Group-WhatsApp-%2322BC18)](#) -->
[![License](https://img.shields.io/badge/license-GPL--3.0-orange)](./LICENSE) [![License](https://img.shields.io/badge/license-GPL--3.0-orange)](./LICENSE)
[![Support](https://img.shields.io/badge/Buy%20me-coffe-orange)](https://app.picpay.com/user/davidsongomes1998) --> [![Support](https://img.shields.io/badge/Buy%20me-coffe-orange)](https://app.picpay.com/user/davidsongomes1998)
<!-- </div> --> </div>
<!-- <div align="center"><img src="./public/images/cover.png"></div>-> --> <div align="center"><img src="./public/images/cover.png"></div>
## WhatsApp-Api-NodeJs ## WhatsApp-Api-NodeJs
This project is based on the [CodeChat](https://github.com/code-chat-br/whatsapp-api). The original project is an implementation of [Baileys](https://github.com/adiwajshing/Baileys), serving as a Restful API service that controls WhatsApp functions.</br> This project is based on the [CodeChat](https://github.com/code-chat-br/whatsapp-api). The original project is an implementation of [Baileys](https://github.com/WhiskeySockets/Baileys), serving as a Restful API service that controls WhatsApp functions.</br>
The code allows the creation of multiservice chats, service bots, or any other system that utilizes WhatsApp. The documentation provides instructions on how to set up and use the project, as well as additional information about its features and configuration options. The code allows the creation of multiservice chats, service bots, or any other system that utilizes WhatsApp. The documentation provides instructions on how to set up and use the project, as well as additional information about its features and configuration options.
## Infrastructure ## Infrastructure
@ -44,7 +41,7 @@ sudo usermod -aG docker ${USER}
### Nodejs installation ### Nodejs installation
```sh ```sh
nvm install 16.17.0 nvm install 16.18.1
``` ```
### pm2 installation ### pm2 installation
@ -72,7 +69,7 @@ Using the database is optional.
Cloning the Repository Cloning the Repository
``` ```
git clone https://github.com/code-chat-br/whatsapp-api.git git clone https://github.com/EvolutionAPI/evolution-api.git
``` ```
Go to the project directory and install all dependencies.</br> Go to the project directory and install all dependencies.</br>
@ -91,7 +88,7 @@ npm run start
npm run start:prod npm run start:prod
# pm2 # pm2
pm2 start 'npm run start:prod' --name ApiCodechat pm2 start 'npm run start:prod' --name ApiEvolution
``` ```
## Authentication ## Authentication
@ -121,7 +118,7 @@ Content-Type: application/json
apikey: t8OOEeISKzpmc3jjcMqBWYSaJH2PIxns apikey: t8OOEeISKzpmc3jjcMqBWYSaJH2PIxns
{ {
"instanceName": "codechat" "instanceName": "evolution"
} }
``` ```
##### cURL ##### cURL
@ -131,7 +128,7 @@ curl --location --request POST 'http://localhost:8080/instance/create' \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--header 'apikey: t8OOEeISKzpmc3jjcMqBWYSaJH2PIxns' \ --header 'apikey: t8OOEeISKzpmc3jjcMqBWYSaJH2PIxns' \
--data-raw '{ --data-raw '{
"instanceName": "codechat" "instanceName": "evolution"
}' }'
``` ```
### Response ### Response
@ -139,7 +136,7 @@ curl --location --request POST 'http://localhost:8080/instance/create' \
```ts ```ts
{ {
"instance": { "instance": {
"instanceName": "codechat", "instanceName": "evolution",
"status": "created" "status": "created"
}, },
"hash": { "hash": {
@ -155,23 +152,23 @@ curl --location --request POST 'http://localhost:8080/instance/create' \
##### HTTP ##### HTTP
```http ```http
GET /instance/connect/codechat HTTP/1.1 GET /instance/connect/evolution HTTP/1.1
Host: localhost:8080 Host: localhost:8080
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...] Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...]
``` ```
```http ```http
GET /instance/connect/codechat HTTP/1.1 GET /instance/connect/evolution HTTP/1.1
Host: localhost:8080 Host: localhost:8080
apikey: 88513847-1B0E-4188-8D76-4A2750C9B6C3 apikey: 88513847-1B0E-4188-8D76-4A2750C9B6C3
``` ```
##### cURL ##### cURL
```bash ```bash
curl --location --request GET 'http://localhost:8080/instance/connect/codechat' \ curl --location --request GET 'http://localhost:8080/instance/connect/evolution' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...]' --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...]'
``` ```
```bash ```bash
curl --location --request GET 'http://localhost:8080/instance/connect/codechat' \ curl --location --request GET 'http://localhost:8080/instance/connect/evolution' \
--header 'apikey: 88513847-1B0E-4188-8D76-4A2750C9B6C3' --header 'apikey: 88513847-1B0E-4188-8D76-4A2750C9B6C3'
``` ```
@ -188,7 +185,7 @@ curl --location --request GET 'http://localhost:8080/instance/connect/codechat'
- [docker run](./docker.sh) - [docker run](./docker.sh)
- [docker-compose](./docker-compose.yml) - [docker-compose](./docker-compose.yml)
- [env for docker](./Docker/.env) - [env for docker](./Docker/.env)
- [DockerHub-codechat/api](https://hub.docker.com/r/codechat/api) - [DockerHub Evolution API](https://hub.docker.com/repository/docker/davidsongomes/evolution-api)
After building the application, in the same directory as the files above, run the following command: After building the application, in the same directory as the files above, run the following command:
```sh ```sh
@ -198,17 +195,17 @@ docker-compose up
| | | | | |
|-----|---| |-----|---|
| Send Text | ✔ | | Send Text | ✔ |
| Send Buttons | ✔ |
| Send Template | ❌ | | Send Template | ❌ |
| Send Media: audio - video - image - document - gif <br></br>base64: ```true``` | ✔ | | Send Media: audio - video - image - document - gif <br></br>base64: ```true``` | ✔ |
| Send Media File | ❌ | | Send Media File | ❌ |
| Send Audio type WhatsApp | ✔ | | Send Audio type WhatsApp | ✔ |
| Send Location | ✔ | | Send Location | ✔ |
| Send List | ✔ |
| Send Link Preview | ✔ | | Send Link Preview | ✔ |
| Send Contact | ✔ | | Send Contact | ✔ |
| Send Reaction - emoji | ✔ | | Send Reaction - emoji | ✔ |
| Send Poll Message | ✔ | | Send Poll Message | ✔ |
| Send Buttons (Deprecated) | ❌ |
| Send List (Deprecated) | ❌ |
## Postman collections ## Postman collections
- [Postman Json](./postman.json) - [Postman Json](./postman.json)
@ -236,6 +233,33 @@ docker-compose up
| GROUP_PARTICIPANTS_UPDATE | group-participants.update | JSON | Notifies when an action occurs involving a participant</br>'add' | 'remove' | 'promote' | 'demote' | | GROUP_PARTICIPANTS_UPDATE | group-participants.update | JSON | Notifies when an action occurs involving a participant</br>'add' | 'remove' | 'promote' | 'demote' |
| NEW_TOKEN | new.jwt | JSON | Notifies when the token (jwt) is updated | NEW_TOKEN | new.jwt | JSON | Notifies when the token (jwt) is updated
## Webhook Routes
When enabling the WEBHOOK_BY_EVENTS options in the global and local webhooks, the following paths will be added at the end of the webhook.
<br><br>
Example:
https://sub.domain.com/webhook-test/exclusive-webhook-code/qrcode-updated
| Name | Path |
|------|-------|
| APPLICATION_STARTUP | /application-startup |
| QRCODE_UPDATED | /qrcode-updated |
| CONNECTION_UPDATE | /connection-update |
| MESSAGES_SET | /messages-set |
| MESSAGES_UPSERT | /messages-upsert |
| MESSAGES_UPDATE | /messages-update |
| SEND_MESSAGE | /send-message |
| CONTACTS_SET | /contacts-set |
| CONTACTS_UPSERT | /contacts-upsert |
| CONTACTS_UPDATE | /contacts-update |
| PRESENCE_UPDATE | /presence-update |
| CHATS_SET | /chats-set |
| CHATS_UPDATE | /chats-update |
| CHATS_UPSERT | /chats-upsert |
| GROUPS_UPSERT | /groups-upsert |
| GROUPS_UPDATE | /groups-update |
| GROUP_PARTICIPANTS_UPDATE | /groups-participants-update |
| NEW_TOKEN | /new-token |
## Env File ## Env File
See additional settings that can be applied through the **env** file by clicking **[here](./src/dev-env.yml)**. See additional settings that can be applied through the **env** file by clicking **[here](./src/dev-env.yml)**.
@ -254,8 +278,6 @@ This code was produced based on the baileys library and it is still under develo
# Donate to the project. # Donate to the project.
#### Pix: 2b526ada-4ef4-4db4-bbeb-f60da2421fce
#### PicPay #### PicPay
<div align="center"> <div align="center">

View File

@ -5,14 +5,139 @@ networks:
driver: bridge driver: bridge
services: services:
base: api:
container_name: evolution_api container_name: evolution_api
image: davidsongomes/evolution-api:latest image: evolution/api:local
ports: ports:
- 8080:8080 - 8080:8080
volumes: volumes:
- /data/instances:/evolution/instances - evolution_instances:/evolution/instances
- evolution_store:/evolution/store
depends_on:
- mongodb
- redis
environment:
# Determine how long the instance should be deleted from memory in case of no connection.
# Default time: 5 minutes
# If you don't even want an expiration, enter the value false
- DEL_INSTANCE=5 # or false
# Temporary data storage
- STORE_MESSAGES=true
- STORE_MESSAGE_UP=true
- STORE_CONTACTS=true
- STORE_CHATS=true
- CLEAN_STORE_CLEANING_INTERVAL=7200 # seconds === 2h
- CLEAN_STORE_MESSAGES=true
- CLEAN_STORE_MESSAGE_UP=true
- CLEAN_STORE_CONTACTS=true
- CLEAN_STORE_CHATS=true
# Permanent data storage
- DATABASE_ENABLED=true
- DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true
- DATABASE_CONNECTION_DB_PREFIX_NAME=evolution
# Choose the data you want to save in the application's database or store
- DATABASE_SAVE_DATA_INSTANCE=true
- DATABASE_SAVE_DATA_OLD_MESSAGE=false
- DATABASE_SAVE_DATA_NEW_MESSAGE=true
- DATABASE_SAVE_MESSAGE_UPDATE=true
- DATABASE_SAVE_DATA_CONTACTS=true
- DATABASE_SAVE_DATA_CHATS=true
- REDIS_ENABLED=true
- REDIS_URI=redis://redis:6379
- REDIS_PREFIX_KEY=evolution
# Webhook Settings
# Define a global webhook that will listen for enabled events from all instances
- WEBHOOK_GLOBAL_URL=url
- WEBHOOK_GLOBAL_ENABLED=false
# With this option activated, you work with a url per webhook event, respecting the global url and the name of each event
- WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false
# Automatically maps webhook paths
# Set the events you want to hear
- WEBHOOK_EVENTS_STATUS_INSTANCE=true
- WEBHOOK_EVENTS_APPLICATION_STARTUP=false
- WEBHOOK_EVENTS_QRCODE_UPDATED=true
- WEBHOOK_EVENTS_MESSAGES_SET=true
- WEBHOOK_EVENTS_MESSAGES_UPDATE=true
- WEBHOOK_EVENTS_MESSAGES_UPSERT=true
- WEBHOOK_EVENTS_SEND_MESSAGE=true
- WEBHOOK_EVENTS_CONTACTS_SET=true
- WEBHOOK_EVENTS_CONTACTS_UPSERT=true
- WEBHOOK_EVENTS_CONTACTS_UPDATE=true
- WEBHOOK_EVENTS_PRESENCE_UPDATE=true
- WEBHOOK_EVENTS_CHATS_SET=true
- WEBHOOK_EVENTS_CHATS_UPSERT=true
- WEBHOOK_EVENTS_CHATS_UPDATE=true
- WEBHOOK_EVENTS_CONNECTION_UPDATE=true
- WEBHOOK_EVENTS_GROUPS_UPSERT=true
- WEBHOOK_EVENTS_GROUPS_UPDATE=true
- WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true
# This event fires every time a new token is requested via the refresh route
- WEBHOOK_EVENTS_NEW_JWT_TOKEN=true
# Name that will be displayed on smartphone connection
- CONFIG_SESSION_PHONE_CLIENT="Evolution API"
# Set qrcode display limit
- QRCODE_LIMIT=30
# Defines an authentication type for the api
- AUTHENTICATION_TYPE=apikey # jwt or apikey
# Define a global apikey to access all instances
# OBS: This key must be inserted in the request header to create an instance.
- AUTHENTICATION_API_KEY=B6D711FCDE4D4FD5936544120E713976
# Expose the api key on return from fetch instances
- AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true
# Set the secret key to encrypt and decrypt your token and its expiration time.
- AUTHENTICATION_JWT_EXPIRIN_IN=0 # seconds - 3600s === 1h | zero (0) - never expires
# Set the instance name and webhook url to create an instance in init the application
# With this option activated, you work with a url per webhook event, respecting the local url and the name of each event
- AUTHENTICATION_INSTANCE_WEBHOOK_BY_EVENTS=false
- AUTHENTICATION_INSTANCE_MODE=server # container or server
# if you are using container mode, set the container name and the webhook url to default instance
- AUTHENTICATION_INSTANCE_NAME=evolution
- AUTHENTICATION_INSTANCE_WEBHOOK_URL=url
command: ['node', './dist/src/main.js'] command: ['node', './dist/src/main.js']
networks: networks:
- evolution-net - evolution-net
expose:
- 8080
mongodb:
container_name: mongodb
image: mongo
restart: always
volumes:
- evolution_mongodb_data:/data/db
- evolution_mongodb_configdb:/data/configdb
ports:
- 27017:27017
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: root
networks:
- evolution-net
expose:
- 27017
redis:
image: redis:latest
command: >
redis-server
--port 6379
--appendonly yes
--save 900 1
--save 300 10
--save 60 10000
--appendfsync everysec
volumes:
- evolution_redis:/data
container_name: redis
ports:
- 6379:6379
networks:
- evolution-net
volumes:
evolution_instances:
evolution_store:
evolution_mongodb_data:
evolution_mongodb_configdb:
evolution_redis:

18
docker.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
NET='evolution-net'
IMAGE='evolution/api:local'
if !(docker network ls | grep ${NET} > /dev/null)
then
docker network create -d bridge ${NET}
fi
sudo mkdir -p ./docker-data/instances
sudo mkdir -p ./docker-data/mongodb
sudo mkdir -p ./docker-data/mongodb/data
sudo mkdir -p ./docker-data/mongodb/configdb
docker build -t ${IMAGE} .
docker compose up -d

View File

@ -42,6 +42,7 @@
"dependencies": { "dependencies": {
"@adiwajshing/keyed-db": "^0.2.4", "@adiwajshing/keyed-db": "^0.2.4",
"@evolution/base": "github:WhiskeySockets/Baileys", "@evolution/base": "github:WhiskeySockets/Baileys",
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@hapi/boom": "^10.0.1", "@hapi/boom": "^10.0.1",
"axios": "^1.3.5", "axios": "^1.3.5",
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
@ -50,6 +51,7 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"eventemitter2": "^6.4.9", "eventemitter2": "^6.4.9",
"exiftool-vendored": "^22.0.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"hbs": "^4.2.0", "hbs": "^4.2.0",
@ -66,6 +68,7 @@
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"redis": "^4.6.5", "redis": "^4.6.5",
"sharp": "^0.30.7",
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
import { load } from 'js-yaml'; import { load } from 'js-yaml';
import { join } from 'path'; import { join } from 'path';
import { SRC_DIR } from './path.config'; import { SRC_DIR } from './path.config';
import { isBooleanString } from 'class-validator';
export type HttpServer = { TYPE: 'http' | 'https'; PORT: number }; export type HttpServer = { TYPE: 'http' | 'https'; PORT: number };
@ -28,8 +29,16 @@ export type SaveData = {
}; };
export type StoreConf = { export type StoreConf = {
MESSAGES: boolean;
MESSAGE_UP: boolean;
CONTACTS: boolean;
CHATS: boolean;
};
export type CleanStoreConf = {
CLEANING_INTERVAL: number; CLEANING_INTERVAL: number;
MESSAGES: boolean; MESSAGES: boolean;
MESSAGE_UP: boolean;
CONTACTS: boolean; CONTACTS: boolean;
CHATS: boolean; CHATS: boolean;
}; };
@ -82,6 +91,7 @@ export type Instance = {
}; };
export type Auth = { export type Auth = {
API_KEY: ApiKey; API_KEY: ApiKey;
EXPOSE_IN_FETCH_INSTANCES: boolean;
JWT: Jwt; JWT: Jwt;
TYPE: 'jwt' | 'apikey'; TYPE: 'jwt' | 'apikey';
INSTANCE: Instance; INSTANCE: Instance;
@ -105,6 +115,7 @@ export interface Env {
CORS: Cors; CORS: Cors;
SSL_CONF: SslConf; SSL_CONF: SslConf;
STORE: StoreConf; STORE: StoreConf;
CLEAN_STORE: CleanStoreConf;
DATABASE: Database; DATABASE: Database;
REDIS: Redis; REDIS: Redis;
LOG: Log; LOG: Log;
@ -134,7 +145,7 @@ export class ConfigService {
this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD'; this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD';
if (process.env?.DOCKER_ENV === 'true') { if (process.env?.DOCKER_ENV === 'true') {
this.env.SERVER.TYPE = 'http'; this.env.SERVER.TYPE = 'http';
this.env.SERVER.PORT = Number.parseInt(process.env?.SERVER_PORT ?? '8080'); this.env.SERVER.PORT = 8080;
} }
} }
@ -158,13 +169,20 @@ export class ConfigService {
FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN, FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN,
}, },
STORE: { STORE: {
CLEANING_INTERVAL: Number.isInteger(process.env?.STORE_CLEANING_TERMINAL) MESSAGES: process.env?.STORE_MESSAGES === 'true',
? Number.parseInt(process.env.STORE_CLEANING_TERMINAL) MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true',
: undefined,
MESSAGES: process.env?.STORE_MESSAGE === 'true',
CONTACTS: process.env?.STORE_CONTACTS === 'true', CONTACTS: process.env?.STORE_CONTACTS === 'true',
CHATS: process.env?.STORE_CHATS === 'true', CHATS: process.env?.STORE_CHATS === 'true',
}, },
CLEAN_STORE: {
CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL)
? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL)
: undefined,
MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true',
MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true',
CONTACTS: process.env?.CLEAN_STORE_CONTACTS === 'true',
CHATS: process.env?.CLEAN_STORE_CHATS === 'true',
},
DATABASE: { DATABASE: {
CONNECTION: { CONNECTION: {
URI: process.env.DATABASE_CONNECTION_URI, URI: process.env.DATABASE_CONNECTION_URI,
@ -189,8 +207,7 @@ export class ConfigService {
LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[], LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[],
COLOR: process.env?.LOG_COLOR === 'true', COLOR: process.env?.LOG_COLOR === 'true',
}, },
DEL_INSTANCE: DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE)
typeof process.env?.DEL_INSTANCE === 'boolean'
? process.env.DEL_INSTANCE === 'true' ? process.env.DEL_INSTANCE === 'true'
: Number.parseInt(process.env.DEL_INSTANCE), : Number.parseInt(process.env.DEL_INSTANCE),
WEBHOOK: { WEBHOOK: {
@ -234,6 +251,8 @@ export class ConfigService {
API_KEY: { API_KEY: {
KEY: process.env.AUTHENTICATION_API_KEY, KEY: process.env.AUTHENTICATION_API_KEY,
}, },
EXPOSE_IN_FETCH_INSTANCES:
process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true',
JWT: { JWT: {
EXPIRIN_IN: Number.isInteger(process.env?.AUTHENTICATION_JWT_EXPIRIN_IN) EXPIRIN_IN: Number.isInteger(process.env?.AUTHENTICATION_JWT_EXPIRIN_IN)
? Number.parseInt(process.env.AUTHENTICATION_JWT_EXPIRIN_IN) ? Number.parseInt(process.env.AUTHENTICATION_JWT_EXPIRIN_IN)

View File

@ -4,7 +4,7 @@ import { Logger } from '../config/logger.config';
const logger = new Logger('Db Connection'); const logger = new Logger('Db Connection');
export const db = configService.get<Database>('DATABASE'); const db = configService.get<Database>('DATABASE');
export const dbserver = db.ENABLED export const dbserver = db.ENABLED
? mongoose.createConnection(db.CONNECTION.URI, { ? mongoose.createConnection(db.CONNECTION.URI, {
dbName: db.CONNECTION.DB_PREFIX_NAME + '-whatsapp-api', dbName: db.CONNECTION.DB_PREFIX_NAME + '-whatsapp-api',

View File

@ -25,6 +25,13 @@ export class RedisCache {
} }
} }
public async keyExists(key?: string) {
if (key) {
return !!(await this.instanceKeys()).find((i) => i === key);
}
return !!(await this.instanceKeys()).find((i) => i === this.instanceName);
}
public async writeData(field: string, data: any) { public async writeData(field: string, data: any) {
try { try {
const json = JSON.stringify(data, BufferJSON.replacer); const json = JSON.stringify(data, BufferJSON.replacer);

View File

@ -7,7 +7,7 @@
# Choose the server type for the application # Choose the server type for the application
SERVER: SERVER:
TYPE: http # https TYPE: http # https
PORT: 8083 # 443 PORT: 8080 # 443
CORS: CORS:
ORIGIN: ORIGIN:
@ -45,8 +45,15 @@ DEL_INSTANCE: false # or false
# Temporary data storage # Temporary data storage
STORE: STORE:
CLEANING_INTERVAL: 7200 # seconds === 2h MESSAGES: true
MESSAGE: true MESSAGE_UP: true
CONTACTS: true
CHATS: true
CLEAN_STORE:
CLEANING_INTERVAL: 7200 # 7200 seconds === 2h
MESSAGES: true
MESSAGE_UP: true
CONTACTS: true CONTACTS: true
CHATS: true CHATS: true
@ -118,6 +125,8 @@ AUTHENTICATION:
API_KEY: API_KEY:
# OBS: This key must be inserted in the request header to create an instance. # OBS: This key must be inserted in the request header to create an instance.
KEY: B6D711FC-DE4D-4FD5-9365-44120E713976 KEY: B6D711FC-DE4D-4FD5-9365-44120E713976
# Expose the api key on return from fetch instances
EXPOSE_IN_FETCH_INSTANCES: true
# Set the secret key to encrypt and decrypt your token and its expiration time. # Set the secret key to encrypt and decrypt your token and its expiration time.
JWT: JWT:
EXPIRIN_IN: 0 # seconds - 3600s === 1h | zero (0) - never expires EXPIRIN_IN: 0 # seconds - 3600s === 1h | zero (0) - never expires

View File

@ -27,6 +27,36 @@ export const instanceNameSchema: JSONSchema7 = {
properties: { properties: {
instanceName: { type: 'string' }, instanceName: { type: 'string' },
webhook: { type: 'string' }, webhook: { type: 'string' },
events: {
type: 'array',
minItems: 1,
items: {
type: 'string',
enum: [
'APPLICATION_STARTUP',
'QRCODE_UPDATED',
'MESSAGES_SET',
'MESSAGES_UPSERT',
'MESSAGES_UPDATE',
'SEND_MESSAGE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
'PRESENCE_UPDATE',
'CHATS_SET',
'CHATS_UPSERT',
'CHATS_UPDATE',
'CHATS_DELETE',
'GROUPS_UPSERT',
'GROUP_UPDATE',
'GROUP_PARTICIPANTS_UPDATE',
'CONNECTION_UPDATE',
'NEW_JWT_TOKEN',
],
},
},
qrcode: { type: 'boolean', enum: [true, false] },
token: { type: 'string' },
}, },
...isNotEmpty('instanceName'), ...isNotEmpty('instanceName'),
}; };
@ -180,6 +210,24 @@ export const mediaMessageSchema: JSONSchema7 = {
required: ['mediaMessage', 'number'], required: ['mediaMessage', 'number'],
}; };
export const stickerMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
number: { ...numberDefinition },
options: { ...optionsSchema },
stickerMessage: {
type: 'object',
properties: {
image: { type: 'string' },
},
required: ['image'],
...isNotEmpty('image'),
},
},
required: ['stickerMessage', 'number'],
};
export const audioMessageSchema: JSONSchema7 = { export const audioMessageSchema: JSONSchema7 = {
$id: v4(), $id: v4(),
type: 'object', type: 'object',
@ -408,6 +456,36 @@ export const readMessageSchema: JSONSchema7 = {
required: ['readMessages'], required: ['readMessages'],
}; };
export const privacySettingsSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
privacySettings: {
type: 'object',
properties: {
readreceipts: { type: 'string', enum: ['all', 'none'] },
profile: {
type: 'string',
enum: ['all', 'contacts', 'contact_blacklist', 'none'],
},
status: {
type: 'string',
enum: ['all', 'contacts', 'contact_blacklist', 'none'],
},
online: { type: 'string', enum: ['all', 'match_last_seen'] },
last: { type: 'string', enum: ['all', 'contacts', 'contact_blacklist', 'none'] },
groupadd: {
type: 'string',
enum: ['all', 'contacts', 'contact_blacklist', 'none'],
},
},
required: ['readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'],
...isNotEmpty('readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'),
},
},
required: ['privacySettings'],
};
export const archiveChatSchema: JSONSchema7 = { export const archiveChatSchema: JSONSchema7 = {
$id: v4(), $id: v4(),
type: 'object', type: 'object',
@ -650,7 +728,7 @@ export const toggleEphemeralSchema: JSONSchema7 = {
...isNotEmpty('groupJid', 'expiration'), ...isNotEmpty('groupJid', 'expiration'),
}; };
export const updateGroupPicture: JSONSchema7 = { export const updateGroupPictureSchema: JSONSchema7 = {
$id: v4(), $id: v4(),
type: 'object', type: 'object',
properties: { properties: {
@ -661,6 +739,28 @@ export const updateGroupPicture: JSONSchema7 = {
...isNotEmpty('groupJid', 'image'), ...isNotEmpty('groupJid', 'image'),
}; };
export const updateGroupSubjectSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
groupJid: { type: 'string' },
subject: { type: 'string' },
},
required: ['groupJid', 'subject'],
...isNotEmpty('groupJid', 'subject'),
};
export const updateGroupDescriptionSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
groupJid: { type: 'string' },
description: { type: 'string' },
},
required: ['groupJid', 'description'],
...isNotEmpty('groupJid', 'description'),
};
// Webhook Schema // Webhook Schema
export const webhookSchema: JSONSchema7 = { export const webhookSchema: JSONSchema7 = {
$id: v4(), $id: v4(),
@ -668,6 +768,34 @@ export const webhookSchema: JSONSchema7 = {
properties: { properties: {
url: { type: 'string' }, url: { type: 'string' },
enabled: { type: 'boolean', enum: [true, false] }, enabled: { type: 'boolean', enum: [true, false] },
events: {
type: 'array',
minItems: 1,
items: {
type: 'string',
enum: [
'APPLICATION_STARTUP',
'QRCODE_UPDATED',
'MESSAGES_SET',
'MESSAGES_UPSERT',
'MESSAGES_UPDATE',
'SEND_MESSAGE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
'PRESENCE_UPDATE',
'CHATS_SET',
'CHATS_UPSERT',
'CHATS_UPDATE',
'CHATS_DELETE',
'GROUPS_UPSERT',
'GROUP_UPDATE',
'GROUP_PARTICIPANTS_UPDATE',
'CONNECTION_UPDATE',
'NEW_JWT_TOKEN',
],
},
},
}, },
required: ['url', 'enabled'], required: ['url', 'enabled'],
...isNotEmpty('url'), ...isNotEmpty('url'),

View File

@ -65,6 +65,37 @@ export abstract class RouterBroker {
return await execute(instance, ref); return await execute(instance, ref);
} }
public async groupNoValidate<T>(args: DataValidate<T>) {
const { request, ClassRef, schema, execute } = args;
const instance = request.params as unknown as InstanceDto;
const ref = new ClassRef();
Object.assign(ref, request.body);
const v = validate(ref, schema);
if (!v.valid) {
const message: any[] = v.errors.map(({ property, stack, schema }) => {
let message: string;
if (schema['description']) {
message = schema['description'];
} else {
message = stack.replace('instance.', '');
}
return {
property: property.replace('instance.', ''),
message,
};
});
logger.error([...message]);
throw new BadRequestException(...message);
}
return await execute(instance, ref);
}
public async groupValidate<T>(args: DataValidate<T>) { public async groupValidate<T>(args: DataValidate<T>) {
const { request, ClassRef, schema, execute } = args; const { request, ClassRef, schema, execute } = args;

View File

@ -3,6 +3,7 @@ import {
ArchiveChatDto, ArchiveChatDto,
DeleteMessage, DeleteMessage,
NumberDto, NumberDto,
PrivacySettingDto,
ProfileNameDto, ProfileNameDto,
ProfilePictureDto, ProfilePictureDto,
ProfileStatusDto, ProfileStatusDto,
@ -63,11 +64,24 @@ export class ChatController {
return await this.waMonitor.waInstances[instanceName].fetchChats(); return await this.waMonitor.waInstances[instanceName].fetchChats();
} }
public async getBusinessProfile( public async fetchPrivacySettings({ instanceName }: InstanceDto) {
return await this.waMonitor.waInstances[instanceName].fetchPrivacySettings();
}
public async updatePrivacySettings(
{ instanceName }: InstanceDto,
data: PrivacySettingDto,
) {
return await this.waMonitor.waInstances[instanceName].updatePrivacySettings(data);
}
public async fetchBusinessProfile(
{ instanceName }: InstanceDto, { instanceName }: InstanceDto,
data: ProfilePictureDto, data: ProfilePictureDto,
) { ) {
return await this.waMonitor.waInstances[instanceName].getBusinessProfile(data.number); return await this.waMonitor.waInstances[instanceName].fetchBusinessProfile(
data.number,
);
} }
public async updateProfileName({ instanceName }: InstanceDto, data: ProfileNameDto) { public async updateProfileName({ instanceName }: InstanceDto, data: ProfileNameDto) {

View File

@ -1,8 +1,10 @@
import { import {
CreateGroupDto, CreateGroupDto,
GroupDescriptionDto,
GroupInvite, GroupInvite,
GroupJid, GroupJid,
GroupPictureDto, GroupPictureDto,
GroupSubjectDto,
GroupToggleEphemeralDto, GroupToggleEphemeralDto,
GroupUpdateParticipantDto, GroupUpdateParticipantDto,
GroupUpdateSettingDto, GroupUpdateSettingDto,
@ -23,10 +25,29 @@ export class GroupController {
); );
} }
public async updateGroupSubject(instance: InstanceDto, update: GroupSubjectDto) {
return await this.waMonitor.waInstances[instance.instanceName].updateGroupSubject(
update,
);
}
public async updateGroupDescription(
instance: InstanceDto,
update: GroupDescriptionDto,
) {
return await this.waMonitor.waInstances[instance.instanceName].updateGroupDescription(
update,
);
}
public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) { public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid); return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid);
} }
public async fetchAllGroups(instance: InstanceDto) {
return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups();
}
public async inviteCode(instance: InstanceDto, groupJid: GroupJid) { public async inviteCode(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid); return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid);
} }
@ -35,6 +56,12 @@ export class GroupController {
return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode); return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode);
} }
public async acceptInvite(instance: InstanceDto, inviteCode: GroupInvite) {
return await this.waMonitor.waInstances[instance.instanceName].acceptInvite(
inviteCode,
);
}
public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) { public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode( return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(
groupJid, groupJid,

View File

@ -9,6 +9,7 @@ import { WAMonitoringService } from '../services/monitor.service';
import { WAStartupService } from '../services/whatsapp.service'; import { WAStartupService } from '../services/whatsapp.service';
import { WebhookService } from '../services/webhook.service'; import { WebhookService } from '../services/webhook.service';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { wa } from '../types/wa.types';
export class InstanceController { export class InstanceController {
constructor( constructor(
@ -22,12 +23,16 @@ export class InstanceController {
private readonly logger = new Logger(InstanceController.name); private readonly logger = new Logger(InstanceController.name);
public async createInstance({ instanceName, webhook }: InstanceDto) { public async createInstance({
//verifica se modo da instancia é container instanceName,
webhook,
events,
qrcode,
token,
}: InstanceDto) {
const mode = this.configService.get<Auth>('AUTHENTICATION').INSTANCE.MODE; const mode = this.configService.get<Auth>('AUTHENTICATION').INSTANCE.MODE;
if (mode === 'container') { if (mode === 'container') {
//verifica se ja existe uma instancia criada com qualquer nome
if (Object.keys(this.waMonitor.waInstances).length > 0) { if (Object.keys(this.waMonitor.waInstances).length > 0) {
throw new BadRequestException([ throw new BadRequestException([
'Instance already created', 'Instance already created',
@ -44,13 +49,20 @@ export class InstanceController {
this.waMonitor.waInstances[instance.instanceName] = instance; this.waMonitor.waInstances[instance.instanceName] = instance;
this.waMonitor.delInstanceTime(instance.instanceName); this.waMonitor.delInstanceTime(instance.instanceName);
const hash = await this.authService.generateHash({ const hash = await this.authService.generateHash(
{
instanceName: instance.instanceName, instanceName: instance.instanceName,
}); },
token,
);
let getEvents: string[];
if (webhook) { if (webhook) {
try { try {
this.webhookService.create(instance, { enabled: true, url: webhook }); this.webhookService.create(instance, { enabled: true, url: webhook, events });
getEvents = (await this.webhookService.find(instance)).events;
} catch (error) { } catch (error) {
this.logger.log(error); this.logger.log(error);
} }
@ -63,6 +75,7 @@ export class InstanceController {
}, },
hash, hash,
webhook, webhook,
events: getEvents,
}; };
} else { } else {
const instance = new WAStartupService( const instance = new WAStartupService(
@ -74,18 +87,33 @@ export class InstanceController {
this.waMonitor.waInstances[instance.instanceName] = instance; this.waMonitor.waInstances[instance.instanceName] = instance;
this.waMonitor.delInstanceTime(instance.instanceName); this.waMonitor.delInstanceTime(instance.instanceName);
const hash = await this.authService.generateHash({ const hash = await this.authService.generateHash(
{
instanceName: instance.instanceName, instanceName: instance.instanceName,
}); },
token,
);
let getEvents: string[];
if (webhook) { if (webhook) {
try { try {
this.webhookService.create(instance, { enabled: true, url: webhook }); this.webhookService.create(instance, { enabled: true, url: webhook, events });
getEvents = (await this.webhookService.find(instance)).events;
} catch (error) { } catch (error) {
this.logger.log(error); this.logger.log(error);
} }
} }
let getQrcode: wa.QrCode;
if (qrcode) {
await instance.connectToWhatsapp();
await delay(2000);
getQrcode = instance.qrCode;
}
return { return {
instance: { instance: {
instanceName: instance.instanceName, instanceName: instance.instanceName,
@ -93,6 +121,8 @@ export class InstanceController {
}, },
hash, hash,
webhook, webhook,
events: getEvents,
qrcode: getQrcode,
}; };
} }
} }
@ -100,7 +130,7 @@ export class InstanceController {
public async connectToWhatsapp({ instanceName }: InstanceDto) { public async connectToWhatsapp({ instanceName }: InstanceDto) {
try { try {
const instance = this.waMonitor.waInstances[instanceName]; const instance = this.waMonitor.waInstances[instanceName];
const state = instance.connectionStatus?.state; const state = instance?.connectionStatus?.state;
switch (state) { switch (state) {
case 'close': case 'close':
@ -113,12 +143,12 @@ export class InstanceController {
return await this.connectionState({ instanceName }); return await this.connectionState({ instanceName });
} }
} catch (error) { } catch (error) {
this.logger.log(error); this.logger.error(error);
} }
} }
public async connectionState({ instanceName }: InstanceDto) { public async connectionState({ instanceName }: InstanceDto) {
return this.waMonitor.waInstances[instanceName].connectionStatus; return this.waMonitor.waInstances[instanceName]?.connectionStatus;
} }
public async fetchInstances({ instanceName }: InstanceDto) { public async fetchInstances({ instanceName }: InstanceDto) {

View File

@ -11,6 +11,7 @@ import {
SendMediaDto, SendMediaDto,
SendPollDto, SendPollDto,
SendReactionDto, SendReactionDto,
SendStickerDto,
SendTextDto, SendTextDto,
} from '../dto/sendMessage.dto'; } from '../dto/sendMessage.dto';
import { WAMonitoringService } from '../services/monitor.service'; import { WAMonitoringService } from '../services/monitor.service';
@ -32,6 +33,13 @@ export class SendMessageController {
throw new BadRequestException('Owned media must be a url or base64'); throw new BadRequestException('Owned media must be a url or base64');
} }
public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto) {
if (isURL(data.stickerMessage.image) || isBase64(data.stickerMessage.image)) {
return await this.waMonitor.waInstances[instanceName].mediaSticker(data);
}
throw new BadRequestException('Owned media must be a url or base64');
}
public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto) { public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto) {
if (isURL(data.audioMessage.audio) || isBase64(data.audioMessage.audio)) { if (isURL(data.audioMessage.audio) || isBase64(data.audioMessage.audio)) {
return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data); return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data);

View File

@ -1,3 +1,9 @@
import {
WAPrivacyOnlineValue,
WAPrivacyValue,
WAReadReceiptsValue,
} from '@evolution/base';
export class OnWhatsAppDto { export class OnWhatsAppDto {
constructor( constructor(
public readonly jid: string, public readonly jid: string,
@ -47,6 +53,19 @@ export class ArchiveChatDto {
archive: boolean; archive: boolean;
} }
class PrivacySetting {
readreceipts: WAReadReceiptsValue;
profile: WAPrivacyValue;
status: WAPrivacyValue;
online: WAPrivacyOnlineValue;
last: WAPrivacyValue;
groupadd: WAPrivacyValue;
}
export class PrivacySettingDto {
privacySettings: PrivacySetting;
}
export class DeleteMessage { export class DeleteMessage {
id: string; id: string;
fromMe: boolean; fromMe: boolean;

View File

@ -9,6 +9,16 @@ export class GroupPictureDto {
image: string; image: string;
} }
export class GroupSubjectDto {
groupJid: string;
subject: string;
}
export class GroupDescriptionDto {
groupJid: string;
description: string;
}
export class GroupJid { export class GroupJid {
groupJid: string; groupJid: string;
} }

View File

@ -1,4 +1,7 @@
export class InstanceDto { export class InstanceDto {
instanceName: string; instanceName: string;
webhook?: string; webhook?: string;
events?: string[];
qrcode?: boolean;
token?: string;
} }

View File

@ -62,6 +62,12 @@ export class MediaMessage {
export class SendMediaDto extends Metadata { export class SendMediaDto extends Metadata {
mediaMessage: MediaMessage; mediaMessage: MediaMessage;
} }
class Sticker {
image: string;
}
export class SendStickerDto extends Metadata {
stickerMessage: Sticker;
}
class Audio { class Audio {
audio: string; audio: string;

View File

@ -1,4 +1,5 @@
export class WebhookDto { export class WebhookDto {
enabled?: boolean; enabled?: boolean;
url?: string; url?: string;
events?: string[];
} }

View File

@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from 'express';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { INSTANCE_DIR } from '../../config/path.config'; import { INSTANCE_DIR } from '../../config/path.config';
import { db, dbserver } from '../../db/db.connect'; import { dbserver } from '../../db/db.connect';
import { import {
BadRequestException, BadRequestException,
ForbiddenException, ForbiddenException,
@ -10,9 +10,20 @@ import {
} from '../../exceptions'; } from '../../exceptions';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
import { waMonitor } from '../whatsapp.module'; import { waMonitor } from '../whatsapp.module';
import { Database, Redis, configService } from '../../config/env.config';
import { RedisCache } from '../../db/redis.client';
async function getInstance(instanceName: string) { async function getInstance(instanceName: string) {
const exists = waMonitor.waInstances[instanceName]; const db = configService.get<Database>('DATABASE');
const redisConf = configService.get<Redis>('REDIS');
const exists = !!waMonitor.waInstances[instanceName];
if (redisConf.ENABLED) {
const cache = new RedisCache(redisConf, instanceName);
const keyExists = await cache.keyExists();
return exists || keyExists;
}
if (db.ENABLED) { if (db.ENABLED) {
const collection = dbserver const collection = dbserver

View File

@ -5,12 +5,14 @@ export class WebhookRaw {
_id?: string; _id?: string;
url?: string; url?: string;
enabled?: boolean; enabled?: boolean;
events?: string[];
} }
const webhookSchema = new Schema<WebhookRaw>({ const webhookSchema = new Schema<WebhookRaw>({
_id: { type: String, _id: true }, _id: { type: String, _id: true },
url: { type: String, required: true }, url: { type: String, required: true },
enabled: { type: Boolean, required: true }, enabled: { type: Boolean, required: true },
events: { type: [String], required: true },
}); });
export const WebhookModel = dbserver?.model(WebhookRaw.name, webhookSchema, 'webhook'); export const WebhookModel = dbserver?.model(WebhookRaw.name, webhookSchema, 'webhook');

View File

@ -1,5 +1,5 @@
import { join } from 'path'; import { join } from 'path';
import { ConfigService } from '../../config/env.config'; import { ConfigService, StoreConf } from '../../config/env.config';
import { IInsert, Repository } from '../abstract/abstract.repository'; import { IInsert, Repository } from '../abstract/abstract.repository';
import { opendirSync, readFileSync, rmSync } from 'fs'; import { opendirSync, readFileSync, rmSync } from 'fs';
import { ChatRaw, IChatModel } from '../models'; import { ChatRaw, IChatModel } from '../models';
@ -27,6 +27,9 @@ export class ChatRepository extends Repository {
return { insertCount: insert.length }; return { insertCount: insert.length };
} }
const store = this.configService.get<StoreConf>('STORE');
if (store.CHATS) {
data.forEach((chat) => { data.forEach((chat) => {
this.writeStore<ChatRaw>({ this.writeStore<ChatRaw>({
path: join(this.storePath, 'chats', chat.owner), path: join(this.storePath, 'chats', chat.owner),
@ -36,6 +39,9 @@ export class ChatRepository extends Repository {
}); });
return { insertCount: data.length }; return { insertCount: data.length };
}
return { insertCount: 0 };
} catch (error) { } catch (error) {
return error; return error;
} finally { } finally {

View File

@ -1,6 +1,6 @@
import { opendirSync, readFileSync } from 'fs'; import { opendirSync, readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { ConfigService } from '../../config/env.config'; import { ConfigService, StoreConf } from '../../config/env.config';
import { ContactRaw, IContactModel } from '../models'; import { ContactRaw, IContactModel } from '../models';
import { IInsert, Repository } from '../abstract/abstract.repository'; import { IInsert, Repository } from '../abstract/abstract.repository';
@ -27,6 +27,9 @@ export class ContactRepository extends Repository {
return { insertCount: insert.length }; return { insertCount: insert.length };
} }
const store = this.configService.get<StoreConf>('STORE');
if (store.CONTACTS) {
data.forEach((contact) => { data.forEach((contact) => {
this.writeStore({ this.writeStore({
path: join(this.storePath, 'contacts', contact.owner), path: join(this.storePath, 'contacts', contact.owner),
@ -36,6 +39,9 @@ export class ContactRepository extends Repository {
}); });
return { insertCount: data.length }; return { insertCount: data.length };
}
return { insertCount: 0 };
} catch (error) { } catch (error) {
return error; return error;
} finally { } finally {

View File

@ -1,4 +1,4 @@
import { ConfigService } from '../../config/env.config'; import { ConfigService, StoreConf } from '../../config/env.config';
import { join } from 'path'; import { join } from 'path';
import { IMessageModel, MessageRaw } from '../models'; import { IMessageModel, MessageRaw } from '../models';
import { IInsert, Repository } from '../abstract/abstract.repository'; import { IInsert, Repository } from '../abstract/abstract.repository';
@ -47,7 +47,9 @@ export class MessageRepository extends Repository {
return { insertCount: insert.length }; return { insertCount: insert.length };
} }
if (saveDb) { const store = this.configService.get<StoreConf>('STORE');
if (store.MESSAGES) {
data.forEach((msg) => data.forEach((msg) =>
this.writeStore<MessageRaw>({ this.writeStore<MessageRaw>({
path: join(this.storePath, 'messages', msg.owner), path: join(this.storePath, 'messages', msg.owner),

View File

@ -1,4 +1,4 @@
import { ConfigService } from '../../config/env.config'; import { ConfigService, StoreConf } from '../../config/env.config';
import { IMessageUpModel, MessageUpdateRaw } from '../models'; import { IMessageUpModel, MessageUpdateRaw } from '../models';
import { IInsert, Repository } from '../abstract/abstract.repository'; import { IInsert, Repository } from '../abstract/abstract.repository';
import { join } from 'path'; import { join } from 'path';
@ -28,6 +28,9 @@ export class MessageUpRepository extends Repository {
return { insertCount: insert.length }; return { insertCount: insert.length };
} }
const store = this.configService.get<StoreConf>('STORE');
if (store.MESSAGE_UP) {
data.forEach((update) => { data.forEach((update) => {
this.writeStore<MessageUpdateRaw>({ this.writeStore<MessageUpdateRaw>({
path: join(this.storePath, 'message-up', update.owner), path: join(this.storePath, 'message-up', update.owner),
@ -35,6 +38,11 @@ export class MessageUpRepository extends Repository {
data: update, data: update,
}); });
}); });
return { insertCount: data.length };
}
return { insertCount: 0 };
} catch (error) { } catch (error) {
return error; return error;
} }

View File

@ -5,6 +5,7 @@ import {
deleteMessageSchema, deleteMessageSchema,
messageUpSchema, messageUpSchema,
messageValidateSchema, messageValidateSchema,
privacySettingsSchema,
profileNameSchema, profileNameSchema,
profilePictureSchema, profilePictureSchema,
profileStatusSchema, profileStatusSchema,
@ -15,6 +16,7 @@ import {
ArchiveChatDto, ArchiveChatDto,
DeleteMessage, DeleteMessage,
NumberDto, NumberDto,
PrivacySettingDto,
ProfileNameDto, ProfileNameDto,
ProfilePictureDto, ProfilePictureDto,
ProfileStatusDto, ProfileStatusDto,
@ -140,12 +142,34 @@ export class ChatRouter extends RouterBroker {
return res.status(HttpStatus.OK).json(response); return res.status(HttpStatus.OK).json(response);
}) })
// Profile routes // Profile routes
.post(this.routerPath('getBusinessProfile'), ...guards, async (req, res) => { .get(this.routerPath('fetchPrivacySettings'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: null,
ClassRef: InstanceDto,
execute: (instance) => chatController.fetchPrivacySettings(instance),
});
return res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('updatePrivacySettings'), ...guards, async (req, res) => {
const response = await this.dataValidate<PrivacySettingDto>({
request: req,
schema: privacySettingsSchema,
ClassRef: PrivacySettingDto,
execute: (instance, data) =>
chatController.updatePrivacySettings(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('fetchBusinessProfile'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfilePictureDto>({ const response = await this.dataValidate<ProfilePictureDto>({
request: req, request: req,
schema: profilePictureSchema, schema: profilePictureSchema,
ClassRef: ProfilePictureDto, ClassRef: ProfilePictureDto,
execute: (instance, data) => chatController.getBusinessProfile(instance, data), execute: (instance, data) =>
chatController.fetchBusinessProfile(instance, data),
}); });
return res.status(HttpStatus.OK).json(response); return res.status(HttpStatus.OK).json(response);

View File

@ -5,7 +5,9 @@ import {
updateParticipantsSchema, updateParticipantsSchema,
updateSettingsSchema, updateSettingsSchema,
toggleEphemeralSchema, toggleEphemeralSchema,
updateGroupPicture, updateGroupPictureSchema,
updateGroupSubjectSchema,
updateGroupDescriptionSchema,
groupInviteSchema, groupInviteSchema,
} from '../../validate/validate.schema'; } from '../../validate/validate.schema';
import { RouterBroker } from '../abstract/abstract.router'; import { RouterBroker } from '../abstract/abstract.router';
@ -14,6 +16,8 @@ import {
GroupInvite, GroupInvite,
GroupJid, GroupJid,
GroupPictureDto, GroupPictureDto,
GroupSubjectDto,
GroupDescriptionDto,
GroupUpdateParticipantDto, GroupUpdateParticipantDto,
GroupUpdateSettingDto, GroupUpdateSettingDto,
GroupToggleEphemeralDto, GroupToggleEphemeralDto,
@ -35,16 +39,37 @@ export class GroupRouter extends RouterBroker {
res.status(HttpStatus.CREATED).json(response); res.status(HttpStatus.CREATED).json(response);
}) })
.put(this.routerPath('updateGroupSubject'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupSubjectDto>({
request: req,
schema: updateGroupSubjectSchema,
ClassRef: GroupSubjectDto,
execute: (instance, data) => groupController.updateGroupSubject(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.put(this.routerPath('updateGroupPicture'), ...guards, async (req, res) => { .put(this.routerPath('updateGroupPicture'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupPictureDto>({ const response = await this.groupValidate<GroupPictureDto>({
request: req, request: req,
schema: updateGroupPicture, schema: updateGroupPictureSchema,
ClassRef: GroupPictureDto, ClassRef: GroupPictureDto,
execute: (instance, data) => groupController.updateGroupPicture(instance, data), execute: (instance, data) => groupController.updateGroupPicture(instance, data),
}); });
res.status(HttpStatus.CREATED).json(response); res.status(HttpStatus.CREATED).json(response);
}) })
.put(this.routerPath('updateGroupDescription'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupDescriptionDto>({
request: req,
schema: updateGroupDescriptionSchema,
ClassRef: GroupDescriptionDto,
execute: (instance, data) =>
groupController.updateGroupDescription(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('findGroupInfos'), ...guards, async (req, res) => { .get(this.routerPath('findGroupInfos'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJid>({ const response = await this.groupValidate<GroupJid>({
request: req, request: req,
@ -55,6 +80,16 @@ export class GroupRouter extends RouterBroker {
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
}) })
.get(this.routerPath('fetchAllGroups'), ...guards, async (req, res) => {
const response = await this.groupNoValidate<GroupJid>({
request: req,
schema: {},
ClassRef: GroupJid,
execute: (instance) => groupController.fetchAllGroups(instance),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('participants'), ...guards, async (req, res) => { .get(this.routerPath('participants'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJid>({ const response = await this.groupValidate<GroupJid>({
request: req, request: req,
@ -85,6 +120,16 @@ export class GroupRouter extends RouterBroker {
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
}) })
.get(this.routerPath('acceptInvite'), ...guards, async (req, res) => {
const response = await this.inviteCodeValidate<GroupInvite>({
request: req,
schema: groupInviteSchema,
ClassRef: GroupInvite,
execute: (instance, data) => groupController.acceptInvite(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('revokeInviteCode'), ...guards, async (req, res) => { .put(this.routerPath('revokeInviteCode'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJid>({ const response = await this.groupValidate<GroupJid>({
request: req, request: req,

View File

@ -7,7 +7,6 @@ import { HttpStatus } from './index.router';
import { OldToken } from '../services/auth.service'; import { OldToken } from '../services/auth.service';
import { Auth, ConfigService, Database } from '../../config/env.config'; import { Auth, ConfigService, Database } from '../../config/env.config';
import { dbserver } from '../../db/db.connect'; import { dbserver } from '../../db/db.connect';
import { BadRequestException, InternalServerErrorException } from '../../exceptions';
export class InstanceRouter extends RouterBroker { export class InstanceRouter extends RouterBroker {
constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) { constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) {

View File

@ -9,6 +9,7 @@ import {
mediaMessageSchema, mediaMessageSchema,
pollMessageSchema, pollMessageSchema,
reactionMessageSchema, reactionMessageSchema,
stickerMessageSchema,
textMessageSchema, textMessageSchema,
} from '../../validate/validate.schema'; } from '../../validate/validate.schema';
import { import {
@ -21,6 +22,7 @@ import {
SendMediaDto, SendMediaDto,
SendPollDto, SendPollDto,
SendReactionDto, SendReactionDto,
SendStickerDto,
SendTextDto, SendTextDto,
} from '../dto/sendMessage.dto'; } from '../dto/sendMessage.dto';
import { sendMessageController } from '../whatsapp.module'; import { sendMessageController } from '../whatsapp.module';
@ -131,6 +133,16 @@ export class MessageRouter extends RouterBroker {
sendMessageController.sendLinkPreview(instance, data), sendMessageController.sendLinkPreview(instance, data),
}); });
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendSticker'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendStickerDto>({
request: req,
schema: stickerMessageSchema,
ClassRef: SendStickerDto,
execute: (instance, data) => sendMessageController.sendSticker(instance, data),
});
return res.status(HttpStatus.CREATED).json(response); return res.status(HttpStatus.CREATED).json(response);
}); });
} }

View File

@ -56,14 +56,14 @@ export class AuthService {
return { jwt: token }; return { jwt: token };
} }
private async apikey(instance: InstanceDto) { private async apikey(instance: InstanceDto, token?: string) {
const apikey = v4().toUpperCase(); const apikey = token ? token : v4().toUpperCase();
const auth = await this.repository.auth.create({ apikey }, instance.instanceName); const auth = await this.repository.auth.create({ apikey }, instance.instanceName);
if (auth['error']) { if (auth['error']) {
this.logger.error({ this.logger.error({
localError: AuthService.name + '.jwt', localError: AuthService.name + '.apikey',
error: auth['error'], error: auth['error'],
}); });
throw new BadRequestException('Authentication error', auth['error']?.toString()); throw new BadRequestException('Authentication error', auth['error']?.toString());
@ -72,9 +72,11 @@ export class AuthService {
return { apikey }; return { apikey };
} }
public async generateHash(instance: InstanceDto) { public async generateHash(instance: InstanceDto, token?: string) {
const options = this.configService.get<Auth>('AUTHENTICATION'); const options = this.configService.get<Auth>('AUTHENTICATION');
return (await this[options.TYPE](instance)) as { jwt: string } | { apikey: string }; return (await this[options.TYPE](instance, token)) as
| { jwt: string }
| { apikey: string };
} }
public async refreshToken({ oldToken }: OldToken) { public async refreshToken({ oldToken }: OldToken) {

View File

@ -4,7 +4,13 @@ import { INSTANCE_DIR } from '../../config/path.config';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import { join } from 'path'; import { join } from 'path';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { ConfigService, Database, DelInstance, Redis } from '../../config/env.config'; import {
Auth,
ConfigService,
Database,
DelInstance,
Redis,
} from '../../config/env.config';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { NotFoundException } from '../../exceptions'; import { NotFoundException } from '../../exceptions';
import { Db } from 'mongodb'; import { Db } from 'mongodb';
@ -60,7 +66,24 @@ export class WAMonitoringService {
const instances: any[] = []; const instances: any[] = [];
for await (const [key, value] of Object.entries(this.waInstances)) { for await (const [key, value] of Object.entries(this.waInstances)) {
if (value && value.connectionStatus.state === 'open') { if (value) {
if (value.connectionStatus.state === 'open') {
let apikey: string;
if (this.configService.get<Auth>('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) {
const tokenStore = await this.repository.auth.find(key);
apikey = tokenStore.apikey || 'Apikey not found';
instances.push({
instance: {
instanceName: key,
owner: value.wuid,
profileName: (await value.getProfileName()) || 'not loaded',
profilePictureUrl: value.profilePictureUrl,
status: (await value.getProfileStatus()) || '',
apikey,
},
});
} else {
instances.push({ instances.push({
instance: { instance: {
instanceName: key, instanceName: key,
@ -71,6 +94,29 @@ export class WAMonitoringService {
}, },
}); });
} }
} else {
let apikey: string;
if (this.configService.get<Auth>('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) {
const tokenStore = await this.repository.auth.find(key);
apikey = tokenStore.apikey || 'Apikey not found';
instances.push({
instance: {
instanceName: key,
status: value.connectionStatus.state,
apikey,
},
});
} else {
instances.push({
instance: {
instanceName: key,
status: value.connectionStatus.state,
},
});
}
}
}
} }
return instances.find((i) => i.instance.instanceName === instanceName) ?? instances; return instances.find((i) => i.instance.instanceName === instanceName) ?? instances;
@ -176,6 +222,8 @@ export class WAMonitoringService {
} }
await set(dirent.name); await set(dirent.name);
} else {
initInstance();
} }
} }
} catch (error) { } catch (error) {

View File

@ -29,17 +29,19 @@ import makeWASocket, {
WAMessage, WAMessage,
WAMessageUpdate, WAMessageUpdate,
WASocket, WASocket,
getAggregateVotesInPollMessage,
} from '@evolution/base'; } from '@evolution/base';
import { import {
Auth, Auth,
CleanStoreConf,
ConfigService, ConfigService,
ConfigSessionPhone, ConfigSessionPhone,
Database, Database,
QrCode, QrCode,
Redis, Redis,
StoreConf,
Webhook, Webhook,
} from '../../config/env.config'; } from '../../config/env.config';
import fs from 'fs';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config'; import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
@ -53,7 +55,8 @@ import { Boom } from '@hapi/boom';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import { release } from 'os'; import { release } from 'os';
import P from 'pino'; import P from 'pino';
import { execSync } from 'child_process'; import { execSync, exec } from 'child_process';
import ffmpegPath from '@ffmpeg-installer/ffmpeg';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { MessageRaw, MessageUpdateRaw } from '../models/message.model'; import { MessageRaw, MessageUpdateRaw } from '../models/message.model';
import { ContactRaw } from '../models/contact.model'; import { ContactRaw } from '../models/contact.model';
@ -73,12 +76,14 @@ import {
SendTextDto, SendTextDto,
SendPollDto, SendPollDto,
SendLinkPreviewDto, SendLinkPreviewDto,
SendStickerDto,
} from '../dto/sendMessage.dto'; } from '../dto/sendMessage.dto';
import { arrayUnique, isBase64, isURL } from 'class-validator'; import { arrayUnique, isBase64, isURL } from 'class-validator';
import { import {
ArchiveChatDto, ArchiveChatDto,
DeleteMessage, DeleteMessage,
OnWhatsAppDto, OnWhatsAppDto,
PrivacySettingDto,
ReadMessageDto, ReadMessageDto,
WhatsAppNumberDto, WhatsAppNumberDto,
} from '../dto/chat.dto'; } from '../dto/chat.dto';
@ -97,6 +102,8 @@ import {
GroupUpdateParticipantDto, GroupUpdateParticipantDto,
GroupUpdateSettingDto, GroupUpdateSettingDto,
GroupToggleEphemeralDto, GroupToggleEphemeralDto,
GroupSubjectDto,
GroupDescriptionDto,
} from '../dto/group.dto'; } from '../dto/group.dto';
import { MessageUpQuery } from '../repository/messageUp.repository'; import { MessageUpQuery } from '../repository/messageUp.repository';
import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db';
@ -105,6 +112,7 @@ import { WebhookRaw } from '../models/webhook.model';
import { dbserver } from '../../db/db.connect'; import { dbserver } from '../../db/db.connect';
import NodeCache from 'node-cache'; import NodeCache from 'node-cache';
import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db';
import sharp from 'sharp';
export class WAStartupService { export class WAStartupService {
constructor( constructor(
@ -195,6 +203,7 @@ export class WAStartupService {
const data = await this.repository.webhook.find(this.instanceName); const data = await this.repository.webhook.find(this.instanceName);
this.localWebhook.url = data?.url; this.localWebhook.url = data?.url;
this.localWebhook.enabled = data?.enabled; this.localWebhook.enabled = data?.enabled;
this.localWebhook.events = data?.events;
} }
public async setWebhook(data: WebhookRaw) { public async setWebhook(data: WebhookRaw) {
@ -207,14 +216,16 @@ export class WAStartupService {
} }
public async sendDataWebhook<T = any>(event: Events, data: T, local = true) { public async sendDataWebhook<T = any>(event: Events, data: T, local = true) {
const webhook = this.configService.get<Webhook>('WEBHOOK'); const webhookGlobal = this.configService.get<Webhook>('WEBHOOK');
const webhookLocal = this.localWebhook.events;
const we = event.replace(/[\.-]/gm, '_').toUpperCase(); const we = event.replace(/[\.-]/gm, '_').toUpperCase();
const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase();
const instance = this.configService.get<Auth>('AUTHENTICATION').INSTANCE; const instance = this.configService.get<Auth>('AUTHENTICATION').INSTANCE;
if (webhook.EVENTS[we]) { if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) {
if (local && instance.MODE !== 'container') { if (local && instance.MODE !== 'container') {
const { WEBHOOK_BY_EVENTS } = instance; const { WEBHOOK_BY_EVENTS } = instance;
let baseURL; let baseURL;
if (WEBHOOK_BY_EVENTS) { if (WEBHOOK_BY_EVENTS) {
@ -222,6 +233,16 @@ export class WAStartupService {
} else { } else {
baseURL = this.localWebhook.url; baseURL = this.localWebhook.url;
} }
this.logger.log({
local: WAStartupService.name + '.sendDataWebhook-local',
url: baseURL,
event,
instance: this.instance.name,
data,
destination: this.localWebhook.url,
});
try { try {
if (this.localWebhook.enabled && isURL(this.localWebhook.url)) { if (this.localWebhook.enabled && isURL(this.localWebhook.url)) {
const httpService = axios.create({ baseURL }); const httpService = axios.create({ baseURL });
@ -246,10 +267,15 @@ export class WAStartupService {
}); });
} }
} }
}
if (webhookGlobal.GLOBAL?.ENABLED) {
if (webhookGlobal.EVENTS[we]) {
const globalWebhook = this.configService.get<Webhook>('WEBHOOK').GLOBAL; const globalWebhook = this.configService.get<Webhook>('WEBHOOK').GLOBAL;
let globalURL; let globalURL;
if (webhook.GLOBAL.WEBHOOK_BY_EVENTS) { if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) {
globalURL = `${globalWebhook.URL}/${transformedWe}`; globalURL = `${globalWebhook.URL}/${transformedWe}`;
} else { } else {
globalURL = globalWebhook.URL; globalURL = globalWebhook.URL;
@ -264,12 +290,14 @@ export class WAStartupService {
} }
this.logger.log({ this.logger.log({
local: WAStartupService.name + '.sendDataWebhook-global',
url: globalURL, url: globalURL,
event, event,
instance: this.instance.name, instance: this.instance.name,
data, data,
destination: localUrl, destination: localUrl,
}); });
try { try {
if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) {
const httpService = axios.create({ baseURL: globalURL }); const httpService = axios.create({ baseURL: globalURL });
@ -295,6 +323,7 @@ export class WAStartupService {
} }
} }
} }
}
private async connectionUpdate({ private async connectionUpdate({
qr, qr,
@ -411,24 +440,24 @@ export class WAStartupService {
} }
private cleanStore() { private cleanStore() {
const store = this.configService.get<StoreConf>('STORE'); const cleanStore = this.configService.get<CleanStoreConf>('CLEAN_STORE');
const database = this.configService.get<Database>('DATABASE'); const database = this.configService.get<Database>('DATABASE');
if (store?.CLEANING_INTERVAL && !database.ENABLED) { if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) {
setInterval(() => { setInterval(() => {
try { try {
for (const [key, value] of Object.entries(store)) { for (const [key, value] of Object.entries(cleanStore)) {
if (value === true) { if (value === true) {
execSync( execSync(
`rm -rf ${join( `rm -rf ${join(
this.storePath, this.storePath,
key.toLowerCase(), key.toLowerCase().replace('_', '-'),
this.instance.wuid, this.instance.wuid,
)}/*.json`, )}/*.json`,
); );
} }
} }
} catch (error) {} } catch (error) {}
}, (store?.CLEANING_INTERVAL ?? 3600) * 1000); }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000);
} }
} }
@ -455,7 +484,7 @@ export class WAStartupService {
const { version } = await fetchLatestBaileysVersion(); const { version } = await fetchLatestBaileysVersion();
const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE'); const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE');
const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; const browser: WABrowserDescription = [session.CLIENT, 'Chrome', release()];
const socketConfig: UserFacingSocketConfig = { const socketConfig: UserFacingSocketConfig = {
auth: { auth: {
@ -716,6 +745,18 @@ export class WAStartupService {
}; };
for await (const { key, update } of args) { for await (const { key, update } of args) {
if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) {
if (update.pollUpdates) {
const pollCreation = await this.getMessage(key);
console.log('pollCreation: ', pollCreation);
if (pollCreation) {
const pollMessage = getAggregateVotesInPollMessage({
message: pollCreation as proto.IMessage,
pollUpdates: update.pollUpdates,
});
console.log('pollMessage: ', pollMessage);
}
}
const message: MessageUpdateRaw = { const message: MessageUpdateRaw = {
...key, ...key,
status: status[update.status], status: status[update.status],
@ -960,7 +1001,12 @@ export class WAStartupService {
quoted, quoted,
}; };
if (!message['audio'] && !message['poll'] && !message['linkPreview']) { if (
!message['audio'] &&
!message['poll'] &&
!message['linkPreview'] &&
!message['sticker']
) {
if (!message['audio']) { if (!message['audio']) {
return await this.client.sendMessage( return await this.client.sendMessage(
sender, sender,
@ -1105,6 +1151,42 @@ export class WAStartupService {
} }
} }
private async convertToWebP(image: string) {
try {
let imagePath: string;
const outputPath = `${join(process.cwd(), 'temp', 'sticker.webp')}`;
if (isBase64(image)) {
const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, '');
const imageBuffer = Buffer.from(base64Data, 'base64');
imagePath = `${join(process.cwd(), 'temp', 'temp-sticker.png')}`;
await sharp(imageBuffer).toFile(imagePath);
} else {
const response = await axios.get(image, { responseType: 'arraybuffer' });
const imageBuffer = Buffer.from(response.data, 'binary');
imagePath = `${join(process.cwd(), 'temp', 'temp-sticker.png')}`;
await sharp(imageBuffer).toFile(imagePath);
}
await sharp(imagePath).webp().toFile(outputPath);
return outputPath;
} catch (error) {
console.error('Erro ao converter a imagem para WebP:', error);
}
}
public async mediaSticker(data: SendStickerDto) {
const convert = await this.convertToWebP(data.stickerMessage.image);
return await this.sendMessageWithTyping(
data.number,
{
sticker: { url: convert },
},
data?.options,
);
}
public async mediaMessage(data: SendMediaDto) { public async mediaMessage(data: SendMediaDto) {
const generate = await this.prepareMediaMessage(data.mediaMessage); const generate = await this.prepareMediaMessage(data.mediaMessage);
@ -1115,18 +1197,54 @@ export class WAStartupService {
); );
} }
private async processAudio(audio: string) {
let tempAudioPath: string;
let outputAudio: string;
if (isURL(audio)) {
outputAudio = `${join(process.cwd(), 'temp', 'audio.mp4')}`;
tempAudioPath = `${join(process.cwd(), 'temp', 'audioTemp.mp3')}`;
const response = await axios.get(audio, { responseType: 'arraybuffer' });
fs.writeFileSync(tempAudioPath, response.data);
} else {
outputAudio = `${join(process.cwd(), 'temp', 'audio.mp4')}`;
tempAudioPath = `${join(process.cwd(), 'temp', 'audioTemp.mp3')}`;
const audioBuffer = Buffer.from(audio, 'base64');
fs.writeFileSync(tempAudioPath, audioBuffer);
}
return new Promise((resolve, reject) => {
exec(
// `${ffmpegPath.path} -i ${tempAudioPath} -c:a libopus ${outputAudio} -y`,
`${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`,
(error, _stdout, _stderr) => {
fs.unlinkSync(tempAudioPath);
if (error) reject(error);
resolve(outputAudio);
},
);
});
}
public async audioWhatsapp(data: SendAudioDto) { public async audioWhatsapp(data: SendAudioDto) {
const convert = await this.processAudio(data.audioMessage.audio);
if (typeof convert === 'string') {
const audio = fs.readFileSync(convert).toString('base64');
return this.sendMessageWithTyping<AnyMessageContent>( return this.sendMessageWithTyping<AnyMessageContent>(
data.number, data.number,
{ {
audio: isURL(data.audioMessage.audio) audio: Buffer.from(audio, 'base64'),
? { url: data.audioMessage.audio }
: Buffer.from(data.audioMessage.audio, 'base64'),
ptt: true, ptt: true,
mimetype: 'audio/ogg; codecs=opus', // mimetype: 'audio/ogg; codecs=opus',
mimetype: 'audio/mp4',
}, },
{ presence: 'recording', delay: data?.options?.delay }, { presence: 'recording', delay: data?.options?.delay },
); );
} else {
throw new InternalServerErrorException(convert);
}
} }
public async buttonMessage(data: SendButtonDto) { public async buttonMessage(data: SendButtonDto) {
@ -1442,7 +1560,31 @@ export class WAStartupService {
return await this.repository.chat.find({ where: { owner: this.instance.wuid } }); return await this.repository.chat.find({ where: { owner: this.instance.wuid } });
} }
public async getBusinessProfile(number: string) { public async fetchPrivacySettings() {
return await this.client.fetchPrivacySettings();
}
public async updatePrivacySettings(settings: PrivacySettingDto) {
try {
await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts);
await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile);
await this.client.updateStatusPrivacy(settings.privacySettings.status);
await this.client.updateOnlinePrivacy(settings.privacySettings.online);
await this.client.updateLastSeenPrivacy(settings.privacySettings.last);
await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd);
// reinicia a instancia
return { update: 'success', data: await this.client.fetchPrivacySettings() };
} catch (error) {
throw new InternalServerErrorException(
'Error updating privacy settings',
error.toString(),
);
}
}
public async fetchBusinessProfile(number: string) {
try { try {
let jid; let jid;
@ -1562,7 +1704,36 @@ export class WAStartupService {
return { update: 'success' }; return { update: 'success' };
} catch (error) { } catch (error) {
throw new InternalServerErrorException('Error creating group', error.toString()); throw new InternalServerErrorException(
'Error update group picture',
error.toString(),
);
}
}
public async updateGroupSubject(data: GroupSubjectDto) {
try {
await this.client.groupUpdateSubject(data.groupJid, data.subject);
return { update: 'success' };
} catch (error) {
throw new InternalServerErrorException(
'Error updating group subject',
error.toString(),
);
}
}
public async updateGroupDescription(data: GroupDescriptionDto) {
try {
await this.client.groupUpdateDescription(data.groupJid, data.description);
return { update: 'success' };
} catch (error) {
throw new InternalServerErrorException(
'Error updating group description',
error.toString(),
);
} }
} }
@ -1577,6 +1748,14 @@ export class WAStartupService {
} }
} }
public async fetchAllGroups() {
try {
return await this.client.groupFetchAllParticipating();
} catch (error) {
throw new NotFoundException('Error fetching group', error.toString());
}
}
public async inviteCode(id: GroupJid) { public async inviteCode(id: GroupJid) {
try { try {
const code = await this.client.groupInviteCode(id.groupJid); const code = await this.client.groupInviteCode(id.groupJid);
@ -1594,6 +1773,14 @@ export class WAStartupService {
} }
} }
public async acceptInvite(id: GroupInvite) {
try {
return await this.client.groupAcceptInvite(id.inviteCode);
} catch (error) {
throw new NotFoundException('No invite info', id.inviteCode);
}
}
public async revokeInviteCode(id: GroupJid) { public async revokeInviteCode(id: GroupJid) {
try { try {
const inviteCode = await this.client.groupRevokeInvite(id.groupJid); const inviteCode = await this.client.groupRevokeInvite(id.groupJid);

View File

@ -34,7 +34,7 @@ export declare namespace wa {
profilePictureUrl?: string; profilePictureUrl?: string;
}; };
export type LocalWebHook = { enabled?: boolean; url?: string }; export type LocalWebHook = { enabled?: boolean; url?: string; events?: string[] };
export type StateConnection = { export type StateConnection = {
instance?: string; instance?: string;

24
start.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/sh
if [ "$DOCKER_ENV" = "true" ];
then
echo "Enabling environment variables for Docker"
echo "DOCKER_ENV=$DOCKER_ENV"
echo
else
cp ./src/env.yml ./dist/src
fi
echo "> removing dist"
rm -rf ./dist
echo
echo "> transpiling..."
npm run build
echo
echo "> Successfully build "
echo
echo "> Starting application..."
echo
node ./dist/src/main.js

BIN
temp/audio.mp4 Normal file

Binary file not shown.

BIN
temp/sticker.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
temp/sticker.webp_original Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB