Compare commits

..

42 Commits
1.0.9 ... 1.1.0

Author SHA1 Message Date
Davidson Gomes
e05690af4c Merge branch 'release/1.1.0' 2023-06-21 11:18:25 -03:00
Davidson Gomes
8e65dd0546 changelog release 1.1.0 2023-06-21 11:18:11 -03:00
Davidson Gomes
509442a2f3 changelog release 1.1.0 2023-06-21 11:17:40 -03:00
Davidson Gomes
a5102d1b94 new postman file 2023-06-21 11:16:44 -03:00
Davidson Gomes
19ee2b3f34 fix: fix for container mode also work only with files 2023-06-20 19:31:39 -03:00
Davidson Gomes
5d29c2d881 fix: fix for container mode also work only with files 2023-06-20 19:29:47 -03:00
Davidson Gomes
a08bbab9dc feat: route to send Sticker 2023-06-20 16:47:26 -03:00
Davidson Gomes
84f6394f1f fix: Adjustment in the recording of temporary files and periodic cleaning 2023-06-20 14:57:30 -03:00
Davidson Gomes
d359949310 fix: Adjustment in the recording of temporary files and periodic cleaning 2023-06-20 14:57:19 -03:00
Davidson Gomes
30cd8a03eb feat: the created instance token can now also be optionally defined manually in the creation endpoint 2023-06-20 09:07:26 -03:00
Davidson Gomes
1b7015c0df conversion of audios for sending recorded audio, now it is possible to send mp3 audios and not just ogg 2023-06-19 22:37:42 -03:00
Davidson Gomes
55b14641e0 conversion of audios for sending recorded audio, now it is possible to send mp3 audios and not just ogg 2023-06-19 22:30:59 -03:00
Davidson Gomes
4936d7fcc6 conversion of audios for sending recorded audio, now it is possible to send mp3 audios and not just ogg 2023-06-19 22:29:07 -03:00
Davidson Gomes
b8fa43296d conversion of audios for sending recorded audio, now it is possible to send mp3 audios and not just ogg 2023-06-19 22:28:39 -03:00
Davidson Gomes
631dd01c92 feat: added option to generate qrcode as soon as the instance is created 2023-06-13 17:42:30 -03:00
Davidson Gomes
fdc72bc84e feat: added option to generate qrcode as soon as the instance is created 2023-06-13 17:35:13 -03:00
Davidson Gomes
c63da9cd6e feat: added option to generate qrcode as soon as the instance is created 2023-06-13 17:23:52 -03:00
Davidson Gomes
849d570bcb fixed docker files and quoted message option 2023-06-13 14:08:26 -03:00
Davidson Gomes
85e6efb8b0 fixed docker files and quoted message option 2023-06-13 13:14:53 -03:00
Davidson Gomes
485c8c3113 fixed docker files and quoted message option 2023-06-13 13:11:24 -03:00
Davidson Gomes
f2d0a8eb8c fixed the problem of not disabling the global webhook by the variable and apikey only appears if it is enabled to be exposed 2023-06-13 11:52:30 -03:00
Davidson Gomes
9201bc1022 fix sending narrated audio on whatsapp android and ios 2023-06-13 11:41:17 -03:00
Davidson Gomes
2847a95c57 feat: expose api key in fetch instances 2023-06-12 16:07:38 -03:00
Davidson Gomes
fc30bb9852 feat: Added configuration of events by webhook of instances 2023-06-12 14:40:26 -03:00
Davidson Gomes
0f360d34e8 fix: readme 2023-06-12 13:45:51 -03:00
Davidson Gomes
ec435e086f fix: readme 2023-06-12 13:41:33 -03:00
Davidson Gomes
1efe7f7cde fix: readme 2023-06-12 13:35:47 -03:00
Davidson Gomes
5759341c52 feat: accept invite code 2023-06-12 12:08:53 -03:00
Davidson Gomes
ea9ba27f22 feat: Route to update group description 2023-06-12 12:05:52 -03:00
Davidson Gomes
a28bbce1f9 feat: Route to update group subject 2023-06-12 12:02:10 -03:00
Davidson Gomes
75b48aa8ac fix error after logout and try to get status or to connect again 2023-06-12 10:32:47 -03:00
Davidson Gomes
573e56cd8c feat: Route to update the privacy settings 2023-06-11 19:24:19 -03:00
Davidson Gomes
ab28b4c0c5 feat: Route to update the privacy settings 2023-06-11 11:38:26 -03:00
Davidson Gomes
55e36f24a5 feat: Route to fetch all privacy settings 2023-06-11 11:04:52 -03:00
Davidson Gomes
ea8d6bf6c8 feat: Route to fetch all groups that the connection is part of 2023-06-11 11:00:40 -03:00
Davidson Gomes
75b8bcb853 feat: Added conversion of audios for sending recorded audio 2023-06-11 10:36:59 -03:00
Davidson Gomes
2b2cc2effc feat: Added conversion of audios for sending recorded audio 2023-06-11 10:29:49 -03:00
Davidson Gomes
ae1c5908ae feat: Improved fetch instances endpoint and prepare changelog to version 1.1.0 in homolog 2023-06-10 18:56:11 -03:00
Davidson Gomes
f067cb99c8 fix: adjust in docker-compose file and added all variables in dockerfile 2023-06-10 18:11:28 -03:00
Davidson Gomes
7e7d3a1659 fix: adjust in docker-compose file and added all variables in dockerfile 2023-06-10 17:46:45 -03:00
Davidson Gomes
c5f364f3e4 fix: adjust in docker-compose file and added all variables in dockerfile 2023-06-10 15:48:28 -03:00
Davidson Gomes
2a6366ef85 Merge tag '1.0.9' into develop
* Adjust dockerfile variables
2023-06-10 12:06:13 -03:00
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
```
##### cURL
##### cURL
```bash
curl --location --request POST 'http://localhost:8080/instance/create' \
@@ -131,7 +128,7 @@ curl --location --request POST 'http://localhost:8080/instance/create' \
"instanceName": "evolution"
}'
```
### Response
### Response
```ts
{
@@ -139,7 +136,7 @@ curl --location --request POST 'http://localhost:8080/instance/create' \
"instanceName": "evolution",
"status": "created"
},
"hash": {
"hash": {
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...]"
// or
@@ -155,23 +152,23 @@ curl --location --request POST 'http://localhost:8080/instance/create' \
GET /instance/connect/evolution HTTP/1.1
Host: localhost:8080
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...]
```
```
```http
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/evolution' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...]'
```
```bash
```bash
curl --location --request GET 'http://localhost:8080/instance/connect/evolution' \
--header 'apikey: 88513847-1B0E-4188-8D76-4A2750C9B6C3'
```
### Response
```ts
@@ -188,7 +185,7 @@ curl --location --request GET 'http://localhost:8080/instance/connect/codechat'
- [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
```sh
docker-compose up
```
## Send Messages
@@ -198,17 +195,17 @@ docker-compose up
| Send Template | ❌ |
| Send Media: audio - video - image - document - gif <br></br>base64: ```true``` | ✔ |
| Send Media File | ❌ |
| Send Media File | ❌ |
| Send Audio type WhatsApp | ✔ |
| Send Location | ✔ |
| Send Link Preview | ✔ |
| Send Contact | ✔ |
| Send Reaction - emoji | ✔ |
| Send Reaction - emoji | ✔ |
| Send Poll Message | ✔ |
| Send Buttons (Deprecated) | ❌ |
| Send List (Deprecated) | ❌ |
## Postman collections
- [Postman Json](./postman.json)
## Webhook Events
@@ -236,6 +233,33 @@ docker-compose up
## 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)**.
> **⚠Attention⚠:** rename the **dev-env.yml** file to **env.yml**.
@@ -254,8 +278,6 @@ This code was produced based on the baileys library and it is still under develo
#### PicPay
<div align="center">
<div align="center">
<a href="https://app.picpay.com/user/davidsongomes1998" target="_blank" rel="noopener noreferrer">
<img src="./public/images/picpay-image.png" style="width: 50% !important;">
</a>

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
- 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,10 +207,9 @@ 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'
? process.env.DEL_INSTANCE === 'true'
: Number.parseInt(process.env.DEL_INSTANCE),
DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE)
? process.env.DEL_INSTANCE === 'true'
: Number.parseInt(process.env.DEL_INSTANCE),
WEBHOOK: {
GLOBAL: {
URL: process.env?.WEBHOOK_GLOBAL_URL,
@@ -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({
instanceName: instance.instanceName,
});
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({
instanceName: instance.instanceName,
});
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,15 +27,21 @@ export class ChatRepository extends Repository {
return { insertCount: insert.length };
}
data.forEach((chat) => {
this.writeStore<ChatRaw>({
path: join(this.storePath, 'chats', chat.owner),
fileName: chat.id,
data: chat,
});
});
const store = this.configService.get<StoreConf>('STORE');
return { insertCount: data.length };
if (store.CHATS) {
data.forEach((chat) => {
this.writeStore<ChatRaw>({
path: join(this.storePath, 'chats', chat.owner),
fileName: chat.id,
data: chat,
});
});
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,15 +27,21 @@ export class ContactRepository extends Repository {
return { insertCount: insert.length };
}
data.forEach((contact) => {
this.writeStore({
path: join(this.storePath, 'contacts', contact.owner),
fileName: contact.id,
data: contact,
});
});
const store = this.configService.get<StoreConf>('STORE');
return { insertCount: data.length };
if (store.CONTACTS) {
data.forEach((contact) => {
this.writeStore({
path: join(this.storePath, 'contacts', contact.owner),
fileName: contact.id,
data: contact,
});
});
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,13 +28,21 @@ export class MessageUpRepository extends Repository {
return { insertCount: insert.length };
}
data.forEach((update) => {
this.writeStore<MessageUpdateRaw>({
path: join(this.storePath, 'message-up', update.owner),
fileName: update.id,
data: update,
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),
fileName: update.id,
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,16 +66,56 @@ export class WAMonitoringService {
const instances: any[] = [];
for await (const [key, value] of Object.entries(this.waInstances)) {
if (value && value.connectionStatus.state === 'open') {
instances.push({
instance: {
instanceName: key,
owner: value.wuid,
profileName: (await value.getProfileName()) || 'not loaded',
profilePictureUrl: value.profilePictureUrl,
status: (await value.getProfileStatus()) || '',
},
});
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,
owner: value.wuid,
profileName: (await value.getProfileName()) || 'not loaded',
profilePictureUrl: value.profilePictureUrl,
status: (await value.getProfileStatus()) || '',
},
});
}
} 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,
},
});
}
}
}
}
@@ -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,52 +267,60 @@ export class WAStartupService {
});
}
}
const globalWebhook = this.configService.get<Webhook>('WEBHOOK').GLOBAL;
let globalURL;
}
if (webhook.GLOBAL.WEBHOOK_BY_EVENTS) {
globalURL = `${globalWebhook.URL}/${transformedWe}`;
} else {
globalURL = globalWebhook.URL;
}
if (webhookGlobal.GLOBAL?.ENABLED) {
if (webhookGlobal.EVENTS[we]) {
const globalWebhook = this.configService.get<Webhook>('WEBHOOK').GLOBAL;
let localUrl;
let globalURL;
if (instance.MODE === 'container') {
localUrl = instance.WEBHOOK_URL;
} else {
localUrl = this.localWebhook.url;
}
if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) {
globalURL = `${globalWebhook.URL}/${transformedWe}`;
} else {
globalURL = globalWebhook.URL;
}
this.logger.log({
url: globalURL,
event,
instance: this.instance.name,
data,
destination: localUrl,
});
try {
if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) {
const httpService = axios.create({ baseURL: globalURL });
await httpService.post('', {
event,
instance: this.instance.name,
data,
destination: localUrl,
let localUrl;
if (instance.MODE === 'container') {
localUrl = instance.WEBHOOK_URL;
} else {
localUrl = this.localWebhook.url;
}
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 });
await httpService.post('', {
event,
instance: this.instance.name,
data,
destination: localUrl,
});
}
} catch (error) {
this.logger.error({
local: WAStartupService.name + '.sendDataWebhook-global',
message: error?.message,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
error: error?.errno,
stack: error?.stack,
name: error?.name,
url: globalURL,
});
}
} catch (error) {
this.logger.error({
local: WAStartupService.name + '.sendDataWebhook-global',
message: error?.message,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
error: error?.errno,
stack: error?.stack,
name: error?.name,
url: globalURL,
});
}
}
}
@@ -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) {
return this.sendMessageWithTyping<AnyMessageContent>(
data.number,
{
audio: isURL(data.audioMessage.audio)
? { url: data.audioMessage.audio }
: Buffer.from(data.audioMessage.audio, 'base64'),
ptt: true,
mimetype: 'audio/ogg; codecs=opus',
},
{ presence: 'recording', delay: data?.options?.delay },
);
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: Buffer.from(audio, 'base64'),
ptt: true,
// 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