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*
lerna-debug.log*
/store/*
/docker-compose-data
# Package
/yarn.lock
/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)
### Fixed

View File

@ -12,11 +12,17 @@ LOG_COLOR=true
DEL_INSTANCE=5
# Temporary data storage
STORE_CLEANING_INTERVAL=7200 # seconds ===2h
STORE_MESSAGE=true
STORE_MESSAGES=true
STORE_MESSAGE_UP=true
STORE_CONTACTS=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
DATABASE_ENABLED=false
DATABASE_CONNECTION_URI='<uri>'
@ -70,6 +76,7 @@ AUTHENTICATION_TYPE='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='t8OOEeISKzpmc3jjcMqBWYSaJsafdefer'
AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true
## 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_SECRET='L0YWtjb2w554WFqPG'

View File

@ -9,6 +9,9 @@ COPY ./package.json .
ENV DOCKER_ENV=true
ENV SERVER_TYPE="http"
ENV SERVER_PORT=8080
ENV CORS_ORIGIN="*"
ENV CORS_METHODS="POST,GET,PUT,DELETE"
ENV CORS_CREDENTIALS=true
@ -16,12 +19,18 @@ ENV CORS_CREDENTIALS=true
ENV LOG_LEVEL="ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK"
ENV LOG_COLOR=true
ENV DEL_INSTANCE=false
ENV DEL_INSTANCE=$DEL_INSTANCE
ENV STORE_CLEANING_INTERVAL=7200
ENV STORE_MESSAGE=true
ENV STORE_CONTACTS=true
ENV STORE_CHATS=true
ENV STORE_MESSAGES=$STORE_MESSAGE
ENV STORE_MESSAGE_UP=$STORE_MESSAGE_UP
ENV STORE_CONTACTS=$STORE_CONTACTS
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_CONNECTION_URI=$DATABASE_CONNECTION_URI
@ -37,7 +46,7 @@ ENV REDIS_ENABLED=$REDIS_ENABLED
ENV REDIS_URI=$REDIS_URI
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_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_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 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_EXPOSE_IN_FETCH_INSTANCES=$AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES
ENV AUTHENTICATION_JWT_EXPIRIN_IN=0
ENV AUTHENTICATION_JWT_SECRET="L0YWtjb2w554WFqPG"
ENV AUTHENTICATION_JWT_EXPIRIN_IN=$AUTHENTICATION_JWT_EXPIRIN_IN
ENV AUTHENTICATION_JWT_SECRET="L=0YWt]b2w[WF>#>:&E`"
ENV AUTHENTICATION_INSTANCE_NAME=$AUTHENTICATION_INSTANCE_NAME
ENV AUTHENTICATION_INSTANCE_WEBHOOK_URL=$AUTHENTICATION_INSTANCE_WEBHOOK_URL

View File

@ -1,22 +1,19 @@
<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)](#)
[![Whatsapp Group](https://img.shields.io/badge/Group-WhatsApp-%2322BC18)](#)
<!-- [![Telegram Group](https://img.shields.io/badge/Group-Telegram-%2333C1FF)](#)-->
<!-- [![Whatsapp Group](https://img.shields.io/badge/Group-WhatsApp-%2322BC18)](#) -->
[![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
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.
## Infrastructure
@ -44,7 +41,7 @@ sudo usermod -aG docker ${USER}
### Nodejs installation
```sh
nvm install 16.17.0
nvm install 16.18.1
```
### pm2 installation
@ -72,7 +69,7 @@ Using the database is optional.
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>
@ -91,7 +88,7 @@ npm run start
npm run start:prod
# pm2
pm2 start 'npm run start:prod' --name ApiCodechat
pm2 start 'npm run start:prod' --name ApiEvolution
```
## Authentication
@ -121,7 +118,7 @@ Content-Type: application/json
apikey: t8OOEeISKzpmc3jjcMqBWYSaJH2PIxns
{
"instanceName": "codechat"
"instanceName": "evolution"
}
```
##### cURL
@ -131,7 +128,7 @@ curl --location --request POST 'http://localhost:8080/instance/create' \
--header 'Content-Type: application/json' \
--header 'apikey: t8OOEeISKzpmc3jjcMqBWYSaJH2PIxns' \
--data-raw '{
"instanceName": "codechat"
"instanceName": "evolution"
}'
```
### Response
@ -139,7 +136,7 @@ curl --location --request POST 'http://localhost:8080/instance/create' \
```ts
{
"instance": {
"instanceName": "codechat",
"instanceName": "evolution",
"status": "created"
},
"hash": {
@ -155,23 +152,23 @@ curl --location --request POST 'http://localhost:8080/instance/create' \
##### HTTP
```http
GET /instance/connect/codechat HTTP/1.1
GET /instance/connect/evolution HTTP/1.1
Host: localhost:8080
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...]
```
```http
GET /instance/connect/codechat HTTP/1.1
GET /instance/connect/evolution HTTP/1.1
Host: localhost:8080
apikey: 88513847-1B0E-4188-8D76-4A2750C9B6C3
```
##### cURL
```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. [...]'
```
```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'
```
@ -188,7 +185,7 @@ curl --location --request GET 'http://localhost:8080/instance/connect/codechat'
- [docker run](./docker.sh)
- [docker-compose](./docker-compose.yml)
- [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:
```sh
@ -198,17 +195,17 @@ docker-compose up
| | |
|-----|---|
| Send Text | ✔ |
| Send Buttons | ✔ |
| Send Template | ❌ |
| Send Media: audio - video - image - document - gif <br></br>base64: ```true``` | ✔ |
| Send Media File | ❌ |
| Send Audio type WhatsApp | ✔ |
| Send Location | ✔ |
| Send List | ✔ |
| Send Link Preview | ✔ |
| Send Contact | ✔ |
| Send Reaction - emoji | ✔ |
| Send Poll Message | ✔ |
| Send Buttons (Deprecated) | ❌ |
| Send List (Deprecated) | ❌ |
## Postman collections
- [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' |
| 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
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.
#### Pix: 2b526ada-4ef4-4db4-bbeb-f60da2421fce
#### PicPay
<div align="center">

View File

@ -5,14 +5,139 @@ networks:
driver: bridge
services:
base:
api:
container_name: evolution_api
image: davidsongomes/evolution-api:latest
image: evolution/api:local
ports:
- 8080:8080
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']
networks:
- 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": {
"@adiwajshing/keyed-db": "^0.2.4",
"@evolution/base": "github:WhiskeySockets/Baileys",
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@hapi/boom": "^10.0.1",
"axios": "^1.3.5",
"class-validator": "^0.13.2",
@ -50,6 +51,7 @@
"cross-env": "^7.0.3",
"dayjs": "^1.11.7",
"eventemitter2": "^6.4.9",
"exiftool-vendored": "^22.0.0",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"hbs": "^4.2.0",
@ -66,6 +68,7 @@
"qrcode": "^1.5.1",
"qrcode-terminal": "^0.12.0",
"redis": "^4.6.5",
"sharp": "^0.30.7",
"uuid": "^9.0.0"
},
"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 { join } from 'path';
import { SRC_DIR } from './path.config';
import { isBooleanString } from 'class-validator';
export type HttpServer = { TYPE: 'http' | 'https'; PORT: number };
@ -28,8 +29,16 @@ export type SaveData = {
};
export type StoreConf = {
MESSAGES: boolean;
MESSAGE_UP: boolean;
CONTACTS: boolean;
CHATS: boolean;
};
export type CleanStoreConf = {
CLEANING_INTERVAL: number;
MESSAGES: boolean;
MESSAGE_UP: boolean;
CONTACTS: boolean;
CHATS: boolean;
};
@ -82,6 +91,7 @@ export type Instance = {
};
export type Auth = {
API_KEY: ApiKey;
EXPOSE_IN_FETCH_INSTANCES: boolean;
JWT: Jwt;
TYPE: 'jwt' | 'apikey';
INSTANCE: Instance;
@ -105,6 +115,7 @@ export interface Env {
CORS: Cors;
SSL_CONF: SslConf;
STORE: StoreConf;
CLEAN_STORE: CleanStoreConf;
DATABASE: Database;
REDIS: Redis;
LOG: Log;
@ -134,7 +145,7 @@ export class ConfigService {
this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD';
if (process.env?.DOCKER_ENV === 'true') {
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,
},
STORE: {
CLEANING_INTERVAL: Number.isInteger(process.env?.STORE_CLEANING_TERMINAL)
? Number.parseInt(process.env.STORE_CLEANING_TERMINAL)
: undefined,
MESSAGES: process.env?.STORE_MESSAGE === 'true',
MESSAGES: process.env?.STORE_MESSAGES === 'true',
MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true',
CONTACTS: process.env?.STORE_CONTACTS === '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: {
CONNECTION: {
URI: process.env.DATABASE_CONNECTION_URI,
@ -189,8 +207,7 @@ export class ConfigService {
LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[],
COLOR: process.env?.LOG_COLOR === 'true',
},
DEL_INSTANCE:
typeof process.env?.DEL_INSTANCE === 'boolean'
DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE)
? process.env.DEL_INSTANCE === 'true'
: Number.parseInt(process.env.DEL_INSTANCE),
WEBHOOK: {
@ -234,6 +251,8 @@ export class ConfigService {
API_KEY: {
KEY: process.env.AUTHENTICATION_API_KEY,
},
EXPOSE_IN_FETCH_INSTANCES:
process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true',
JWT: {
EXPIRIN_IN: Number.isInteger(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');
export const db = configService.get<Database>('DATABASE');
const db = configService.get<Database>('DATABASE');
export const dbserver = db.ENABLED
? mongoose.createConnection(db.CONNECTION.URI, {
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) {
try {
const json = JSON.stringify(data, BufferJSON.replacer);

View File

@ -7,7 +7,7 @@
# Choose the server type for the application
SERVER:
TYPE: http # https
PORT: 8083 # 443
PORT: 8080 # 443
CORS:
ORIGIN:
@ -45,8 +45,15 @@ DEL_INSTANCE: false # or false
# Temporary data storage
STORE:
CLEANING_INTERVAL: 7200 # seconds === 2h
MESSAGE: true
MESSAGES: true
MESSAGE_UP: true
CONTACTS: true
CHATS: true
CLEAN_STORE:
CLEANING_INTERVAL: 7200 # 7200 seconds === 2h
MESSAGES: true
MESSAGE_UP: true
CONTACTS: true
CHATS: true
@ -118,6 +125,8 @@ AUTHENTICATION:
API_KEY:
# OBS: This key must be inserted in the request header to create an instance.
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.
JWT:
EXPIRIN_IN: 0 # seconds - 3600s === 1h | zero (0) - never expires

View File

@ -27,6 +27,36 @@ export const instanceNameSchema: JSONSchema7 = {
properties: {
instanceName: { 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'),
};
@ -180,6 +210,24 @@ export const mediaMessageSchema: JSONSchema7 = {
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 = {
$id: v4(),
type: 'object',
@ -408,6 +456,36 @@ export const readMessageSchema: JSONSchema7 = {
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 = {
$id: v4(),
type: 'object',
@ -650,7 +728,7 @@ export const toggleEphemeralSchema: JSONSchema7 = {
...isNotEmpty('groupJid', 'expiration'),
};
export const updateGroupPicture: JSONSchema7 = {
export const updateGroupPictureSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
@ -661,6 +739,28 @@ export const updateGroupPicture: JSONSchema7 = {
...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
export const webhookSchema: JSONSchema7 = {
$id: v4(),
@ -668,6 +768,34 @@ export const webhookSchema: JSONSchema7 = {
properties: {
url: { type: 'string' },
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'],
...isNotEmpty('url'),

View File

@ -65,6 +65,37 @@ export abstract class RouterBroker {
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>) {
const { request, ClassRef, schema, execute } = args;

View File

@ -3,6 +3,7 @@ import {
ArchiveChatDto,
DeleteMessage,
NumberDto,
PrivacySettingDto,
ProfileNameDto,
ProfilePictureDto,
ProfileStatusDto,
@ -63,11 +64,24 @@ export class ChatController {
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,
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) {

View File

@ -1,8 +1,10 @@
import {
CreateGroupDto,
GroupDescriptionDto,
GroupInvite,
GroupJid,
GroupPictureDto,
GroupSubjectDto,
GroupToggleEphemeralDto,
GroupUpdateParticipantDto,
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) {
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) {
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);
}
public async acceptInvite(instance: InstanceDto, inviteCode: GroupInvite) {
return await this.waMonitor.waInstances[instance.instanceName].acceptInvite(
inviteCode,
);
}
public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(
groupJid,

View File

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

View File

@ -11,6 +11,7 @@ import {
SendMediaDto,
SendPollDto,
SendReactionDto,
SendStickerDto,
SendTextDto,
} from '../dto/sendMessage.dto';
import { WAMonitoringService } from '../services/monitor.service';
@ -32,6 +33,13 @@ export class SendMessageController {
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) {
if (isURL(data.audioMessage.audio) || isBase64(data.audioMessage.audio)) {
return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
import { INSTANCE_DIR } from '../../config/path.config';
import { db, dbserver } from '../../db/db.connect';
import { dbserver } from '../../db/db.connect';
import {
BadRequestException,
ForbiddenException,
@ -10,9 +10,20 @@ import {
} from '../../exceptions';
import { InstanceDto } from '../dto/instance.dto';
import { waMonitor } from '../whatsapp.module';
import { Database, Redis, configService } from '../../config/env.config';
import { RedisCache } from '../../db/redis.client';
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) {
const collection = dbserver

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { opendirSync, readFileSync } from 'fs';
import { join } from 'path';
import { ConfigService } from '../../config/env.config';
import { ConfigService, StoreConf } from '../../config/env.config';
import { ContactRaw, IContactModel } from '../models';
import { IInsert, Repository } from '../abstract/abstract.repository';
@ -27,6 +27,9 @@ export class ContactRepository extends Repository {
return { insertCount: insert.length };
}
const store = this.configService.get<StoreConf>('STORE');
if (store.CONTACTS) {
data.forEach((contact) => {
this.writeStore({
path: join(this.storePath, 'contacts', contact.owner),
@ -36,6 +39,9 @@ export class ContactRepository extends Repository {
});
return { insertCount: data.length };
}
return { insertCount: 0 };
} catch (error) {
return error;
} 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 { IMessageModel, MessageRaw } from '../models';
import { IInsert, Repository } from '../abstract/abstract.repository';
@ -47,7 +47,9 @@ export class MessageRepository extends Repository {
return { insertCount: insert.length };
}
if (saveDb) {
const store = this.configService.get<StoreConf>('STORE');
if (store.MESSAGES) {
data.forEach((msg) =>
this.writeStore<MessageRaw>({
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 { IInsert, Repository } from '../abstract/abstract.repository';
import { join } from 'path';
@ -28,6 +28,9 @@ export class MessageUpRepository extends Repository {
return { insertCount: insert.length };
}
const store = this.configService.get<StoreConf>('STORE');
if (store.MESSAGE_UP) {
data.forEach((update) => {
this.writeStore<MessageUpdateRaw>({
path: join(this.storePath, 'message-up', update.owner),
@ -35,6 +38,11 @@ export class MessageUpRepository extends Repository {
data: update,
});
});
return { insertCount: data.length };
}
return { insertCount: 0 };
} catch (error) {
return error;
}

View File

@ -5,6 +5,7 @@ import {
deleteMessageSchema,
messageUpSchema,
messageValidateSchema,
privacySettingsSchema,
profileNameSchema,
profilePictureSchema,
profileStatusSchema,
@ -15,6 +16,7 @@ import {
ArchiveChatDto,
DeleteMessage,
NumberDto,
PrivacySettingDto,
ProfileNameDto,
ProfilePictureDto,
ProfileStatusDto,
@ -140,12 +142,34 @@ export class ChatRouter extends RouterBroker {
return res.status(HttpStatus.OK).json(response);
})
// 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>({
request: req,
schema: profilePictureSchema,
ClassRef: ProfilePictureDto,
execute: (instance, data) => chatController.getBusinessProfile(instance, data),
execute: (instance, data) =>
chatController.fetchBusinessProfile(instance, data),
});
return res.status(HttpStatus.OK).json(response);

View File

@ -5,7 +5,9 @@ import {
updateParticipantsSchema,
updateSettingsSchema,
toggleEphemeralSchema,
updateGroupPicture,
updateGroupPictureSchema,
updateGroupSubjectSchema,
updateGroupDescriptionSchema,
groupInviteSchema,
} from '../../validate/validate.schema';
import { RouterBroker } from '../abstract/abstract.router';
@ -14,6 +16,8 @@ import {
GroupInvite,
GroupJid,
GroupPictureDto,
GroupSubjectDto,
GroupDescriptionDto,
GroupUpdateParticipantDto,
GroupUpdateSettingDto,
GroupToggleEphemeralDto,
@ -35,16 +39,37 @@ export class GroupRouter extends RouterBroker {
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) => {
const response = await this.groupValidate<GroupPictureDto>({
request: req,
schema: updateGroupPicture,
schema: updateGroupPictureSchema,
ClassRef: GroupPictureDto,
execute: (instance, data) => groupController.updateGroupPicture(instance, data),
});
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) => {
const response = await this.groupValidate<GroupJid>({
request: req,
@ -55,6 +80,16 @@ export class GroupRouter extends RouterBroker {
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) => {
const response = await this.groupValidate<GroupJid>({
request: req,
@ -85,6 +120,16 @@ export class GroupRouter extends RouterBroker {
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) => {
const response = await this.groupValidate<GroupJid>({
request: req,

View File

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

View File

@ -9,6 +9,7 @@ import {
mediaMessageSchema,
pollMessageSchema,
reactionMessageSchema,
stickerMessageSchema,
textMessageSchema,
} from '../../validate/validate.schema';
import {
@ -21,6 +22,7 @@ import {
SendMediaDto,
SendPollDto,
SendReactionDto,
SendStickerDto,
SendTextDto,
} from '../dto/sendMessage.dto';
import { sendMessageController } from '../whatsapp.module';
@ -131,6 +133,16 @@ export class MessageRouter extends RouterBroker {
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);
});
}

View File

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

View File

@ -4,7 +4,13 @@ import { INSTANCE_DIR } from '../../config/path.config';
import EventEmitter2 from 'eventemitter2';
import { join } from 'path';
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 { NotFoundException } from '../../exceptions';
import { Db } from 'mongodb';
@ -60,7 +66,24 @@ export class WAMonitoringService {
const instances: any[] = [];
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({
instance: {
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;
@ -176,6 +222,8 @@ export class WAMonitoringService {
}
await set(dirent.name);
} else {
initInstance();
}
}
} catch (error) {

View File

@ -29,17 +29,19 @@ import makeWASocket, {
WAMessage,
WAMessageUpdate,
WASocket,
getAggregateVotesInPollMessage,
} from '@evolution/base';
import {
Auth,
CleanStoreConf,
ConfigService,
ConfigSessionPhone,
Database,
QrCode,
Redis,
StoreConf,
Webhook,
} from '../../config/env.config';
import fs from 'fs';
import { Logger } from '../../config/logger.config';
import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config';
import { existsSync, readFileSync } from 'fs';
@ -53,7 +55,8 @@ import { Boom } from '@hapi/boom';
import EventEmitter2 from 'eventemitter2';
import { release } from 'os';
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 { MessageRaw, MessageUpdateRaw } from '../models/message.model';
import { ContactRaw } from '../models/contact.model';
@ -73,12 +76,14 @@ import {
SendTextDto,
SendPollDto,
SendLinkPreviewDto,
SendStickerDto,
} from '../dto/sendMessage.dto';
import { arrayUnique, isBase64, isURL } from 'class-validator';
import {
ArchiveChatDto,
DeleteMessage,
OnWhatsAppDto,
PrivacySettingDto,
ReadMessageDto,
WhatsAppNumberDto,
} from '../dto/chat.dto';
@ -97,6 +102,8 @@ import {
GroupUpdateParticipantDto,
GroupUpdateSettingDto,
GroupToggleEphemeralDto,
GroupSubjectDto,
GroupDescriptionDto,
} from '../dto/group.dto';
import { MessageUpQuery } from '../repository/messageUp.repository';
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 NodeCache from 'node-cache';
import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db';
import sharp from 'sharp';
export class WAStartupService {
constructor(
@ -195,6 +203,7 @@ export class WAStartupService {
const data = await this.repository.webhook.find(this.instanceName);
this.localWebhook.url = data?.url;
this.localWebhook.enabled = data?.enabled;
this.localWebhook.events = data?.events;
}
public async setWebhook(data: WebhookRaw) {
@ -207,14 +216,16 @@ export class WAStartupService {
}
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 transformedWe = we.replace(/_/gm, '-').toLowerCase();
const instance = this.configService.get<Auth>('AUTHENTICATION').INSTANCE;
if (webhook.EVENTS[we]) {
if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) {
if (local && instance.MODE !== 'container') {
const { WEBHOOK_BY_EVENTS } = instance;
let baseURL;
if (WEBHOOK_BY_EVENTS) {
@ -222,6 +233,16 @@ export class WAStartupService {
} else {
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 {
if (this.localWebhook.enabled && isURL(this.localWebhook.url)) {
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;
let globalURL;
if (webhook.GLOBAL.WEBHOOK_BY_EVENTS) {
if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) {
globalURL = `${globalWebhook.URL}/${transformedWe}`;
} else {
globalURL = globalWebhook.URL;
@ -264,12 +290,14 @@ export class WAStartupService {
}
this.logger.log({
local: WAStartupService.name + '.sendDataWebhook-global',
url: globalURL,
event,
instance: this.instance.name,
data,
destination: localUrl,
});
try {
if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) {
const httpService = axios.create({ baseURL: globalURL });
@ -295,6 +323,7 @@ export class WAStartupService {
}
}
}
}
private async connectionUpdate({
qr,
@ -411,24 +440,24 @@ export class WAStartupService {
}
private cleanStore() {
const store = this.configService.get<StoreConf>('STORE');
const cleanStore = this.configService.get<CleanStoreConf>('CLEAN_STORE');
const database = this.configService.get<Database>('DATABASE');
if (store?.CLEANING_INTERVAL && !database.ENABLED) {
if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) {
setInterval(() => {
try {
for (const [key, value] of Object.entries(store)) {
for (const [key, value] of Object.entries(cleanStore)) {
if (value === true) {
execSync(
`rm -rf ${join(
this.storePath,
key.toLowerCase(),
key.toLowerCase().replace('_', '-'),
this.instance.wuid,
)}/*.json`,
);
}
}
} catch (error) {}
}, (store?.CLEANING_INTERVAL ?? 3600) * 1000);
}, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000);
}
}
@ -455,7 +484,7 @@ export class WAStartupService {
const { version } = await fetchLatestBaileysVersion();
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 = {
auth: {
@ -716,6 +745,18 @@ export class WAStartupService {
};
for await (const { key, update } of args) {
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 = {
...key,
status: status[update.status],
@ -960,7 +1001,12 @@ export class WAStartupService {
quoted,
};
if (!message['audio'] && !message['poll'] && !message['linkPreview']) {
if (
!message['audio'] &&
!message['poll'] &&
!message['linkPreview'] &&
!message['sticker']
) {
if (!message['audio']) {
return await this.client.sendMessage(
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) {
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) {
const convert = await this.processAudio(data.audioMessage.audio);
if (typeof convert === 'string') {
const audio = fs.readFileSync(convert).toString('base64');
return this.sendMessageWithTyping<AnyMessageContent>(
data.number,
{
audio: isURL(data.audioMessage.audio)
? { url: data.audioMessage.audio }
: Buffer.from(data.audioMessage.audio, 'base64'),
audio: Buffer.from(audio, 'base64'),
ptt: true,
mimetype: 'audio/ogg; codecs=opus',
// mimetype: 'audio/ogg; codecs=opus',
mimetype: 'audio/mp4',
},
{ presence: 'recording', delay: data?.options?.delay },
);
} else {
throw new InternalServerErrorException(convert);
}
}
public async buttonMessage(data: SendButtonDto) {
@ -1442,7 +1560,31 @@ export class WAStartupService {
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 {
let jid;
@ -1562,7 +1704,36 @@ export class WAStartupService {
return { update: 'success' };
} 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) {
try {
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) {
try {
const inviteCode = await this.client.groupRevokeInvite(id.groupJid);

View File

@ -34,7 +34,7 @@ export declare namespace wa {
profilePictureUrl?: string;
};
export type LocalWebHook = { enabled?: boolean; url?: string };
export type LocalWebHook = { enabled?: boolean; url?: string; events?: string[] };
export type StateConnection = {
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