Compare commits

..

146 Commits
1.7.2 ... 1.8.1

Author SHA1 Message Date
Davidson Gomes
dfb003fd72 Merge branch 'release/1.8.1' 2024-06-12 19:49:37 -03:00
Davidson Gomes
5239e431c2 feat: update server and monitor services
Updated server.module.ts and monitor.service.ts to improve service initialization and monitoring logic. Modified main.ts to integrate changes. This enhances the application's performance and reliability.
2024-06-12 19:48:56 -03:00
Davidson Gomes
53cc6132f5 Merge tag '1.8.1' into develop
v
2024-06-12 19:03:36 -03:00
Davidson Gomes
0bc1b78db9 Merge branch 'release/1.8.1' 2024-06-12 19:03:30 -03:00
Davidson Gomes
2a9412c81a chore: adjust socket configuration and update baileys version
Updated package.json to include the latest version of baileys for improved functionality. Modified whatsapp.baileys.service.ts to adjust socket configuration, enhancing the stability and performance of the service.
2024-06-12 19:02:36 -03:00
Davidson Gomes
f707cf4109 Merge tag '1.8.1' into develop
v
2024-06-09 14:22:20 -03:00
Davidson Gomes
410cfc8bcb Merge branch 'release/1.8.1' 2024-06-09 14:22:17 -03:00
Davidson Gomes
f1a3fd872f git actions v2 2024-06-09 14:20:48 -03:00
Davidson Gomes
fff0ca5c79 Merge tag '1.8.1' into develop
v
2024-06-08 21:33:12 -03:00
Davidson Gomes
bd069200ea Merge branch 'release/1.8.1' 2024-06-08 21:33:02 -03:00
Davidson Gomes
c898f1e62a v1.8.1 2024-06-08 21:32:50 -03:00
Davidson Gomes
2002e1c38d changelog 2024-06-08 21:31:02 -03:00
Davidson Gomes
1f817df5f6 fix: Correction of variables breaking lines in typebot 2024-06-08 21:27:58 -03:00
Davidson Gomes
8eced6c575 Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2024-06-05 21:48:53 -03:00
Davidson Gomes
afcf6eab71 cacheGroupMetadata 2024-06-05 21:48:37 -03:00
Davidson Gomes
9633412ef6 cacheGroupMetadata 2024-06-05 21:39:33 -03:00
Davidson Gomes
8034e7f587 cacheGroupMetadata 2024-06-05 21:25:55 -03:00
Davidson Gomes
5be01e1c18 Merge pull request #629 from cark7/develop
fix: added validation when sending gif image
2024-06-04 16:59:36 -03:00
Carlos Chinchilla
6371773416 fix: added validation when sending gif image 2024-06-04 15:14:02 -03:00
Davidson Gomes
52230edc5c session worker compatibility 2024-06-03 19:06:00 -03:00
Davidson Gomes
dd123c6a99 Adjust to repository from session worker 2024-06-01 19:47:43 -03:00
Davidson Gomes
5b2a0fdcb1 Adjust to repository from session worker 2024-06-01 19:29:37 -03:00
Davidson Gomes
9354af3bc7 Adjust to repository from session worker 2024-06-01 19:20:59 -03:00
Davidson Gomes
f48f331d43 New method of saving sessions to a file using worker 2024-06-01 13:28:29 -03:00
Davidson Gomes
696261d749 New method of saving sessions to a file using worker 2024-06-01 09:54:56 -03:00
Davidson Gomes
0e9d036fe0 Merge pull request #622 from jrCleber/main
Adiciona templates para Relatório de Bug e Solicitação de Recursos
2024-05-29 10:34:23 -03:00
Cleber Wilson
8e19fe2fb5 labels para filtros adicionada 2024-05-29 10:33:23 -03:00
Cleber Wilson
861b690217 Adiciona templates para Relatório de Bug e Solicitação de Recursos
- Cria um template para Relatório de Bug com os seguintes campos:
  - Termos de aceite
  - Descrição detalhada do problema
  - Expectativa versus observação
  - Capturas de tela/vídeos
  - Versão da API
  - Ambiente
  - Especificações adicionais do ambiente
  - Logs (se aplicável)
  - Notas adicionais

- Cria um template para Solicitação de Recursos com os seguintes campos:
  - Termos de aceite
  - Tipo de recurso
  - Motivação para a solicitação
  - Exemplos de uso
  - Ideias de desenvolvimento
  - Notas adicionais
2024-05-29 10:08:42 -03:00
Davidson Gomes
f7c9541f5e Merge tag '1.8.0' into develop
v
2024-05-27 16:36:08 -03:00
Davidson Gomes
05b5ae8a84 Merge branch 'release/1.8.0' 2024-05-27 16:36:03 -03:00
Davidson Gomes
2e9c14a0a8 fix: security fix in fetch instance with client key when not connected to mongodb 2024-05-27 16:35:59 -03:00
Davidson Gomes
3350cec589 fix: security fix in fetch instance with client key when not connected to mongodb 2024-05-27 16:35:55 -03:00
Davidson Gomes
bd7a42646b Merge tag '1.8.0' into develop
v
2024-05-27 16:24:29 -03:00
Davidson Gomes
2ae4ddee18 Merge branch 'release/1.8.0' 2024-05-27 16:24:25 -03:00
Davidson Gomes
2fb6e2b209 fix: Build in docker for linux/amd64, linux/arm64 platforms 2024-05-27 16:24:21 -03:00
Davidson Gomes
91f869b136 Merge tag '1.8.0' into develop
v
2024-05-27 16:17:30 -03:00
Davidson Gomes
1284a97625 Merge branch 'release/1.8.0' 2024-05-27 16:17:27 -03:00
Davidson Gomes
3a5cfbf36f fix: Build in docker for linux/amd64, linux/arm64 platforms 2024-05-27 16:17:22 -03:00
Davidson Gomes
47b8ecd43b Merge tag '1.8.0' into develop
v
2024-05-27 16:09:16 -03:00
Davidson Gomes
25ce1a4689 Merge branch 'release/1.8.0' 2024-05-27 16:09:12 -03:00
Davidson Gomes
a5156709ab fix: Build in docker for linux/amd64, linux/arm64 platforms 2024-05-27 16:09:07 -03:00
Davidson Gomes
fd2d37d862 Merge tag '1.8.0' into develop
v
2024-05-27 16:07:19 -03:00
Davidson Gomes
87bf433e9a Merge branch 'release/1.8.0' 2024-05-27 16:07:15 -03:00
Davidson Gomes
b5ec79daae fix: Build in docker for linux/amd64, linux/arm64 platforms 2024-05-27 16:07:03 -03:00
Davidson Gomes
3b19200997 Merge tag '1.7.6' into develop
v
2024-05-27 15:54:45 -03:00
Davidson Gomes
0ab040080c Merge branch 'release/1.7.6' 2024-05-27 15:54:42 -03:00
Davidson Gomes
5719896d18 fix: git actions 2024-05-27 15:54:31 -03:00
Davidson Gomes
e9e1cb6ebc Merge tag '1.7.6' into develop
v
2024-05-27 15:51:22 -03:00
Davidson Gomes
ceee6a7e74 Merge branch 'release/1.7.6' 2024-05-27 15:51:19 -03:00
Davidson Gomes
ebfc6d4fa5 fix: git actions 2024-05-27 15:51:06 -03:00
Davidson Gomes
0d62557a15 Merge pull request #609 from stack-app-br/main
chore: build docker image to platforms: linux/amd64,linux/arm64
2024-05-27 15:45:28 -03:00
Davidson Gomes
097c09328f Merge pull request #615 from deivisonrpg/fix-merge-brazil-contacts
fix(chatwoot): add merge_brazil_contacts flag to ChannelStartupService and add logs
2024-05-27 15:34:56 -03:00
Davidson Gomes
732103292b Merge pull request #618 from edisoncm-ti/develop
fix: Correction to Postgres connection string in environment files
2024-05-27 15:34:29 -03:00
edisoncm-ti
3191e9b450 fix: Correction to Postgres connection string in environment files 2024-05-27 15:14:52 -03:00
Davidson Gomes
bf88fdbb31 fix: adjust send presence 2024-05-27 12:57:18 -03:00
Davidson Gomes
4c3fb5e762 correction in message formatting when generated by AI as markdown in typebot 2024-05-27 09:27:42 -03:00
Davidson Gomes
e4b6f4ff0d correction in message formatting when generated by AI as markdown in typebot 2024-05-27 09:23:53 -03:00
Deivison Lincoln
ca0b0d4602 Add merge_brazil_contacts flag to ChannelStartupService 2024-05-25 10:09:37 -03:00
Deivison Lincoln
d7c3779ff7 fix: Add merge_brazil_contacts flag to ChannelStartupService 2024-05-25 10:05:04 -03:00
Davidson Gomes
f0d40eea15 New global mode for rabbitmq events 2024-05-24 18:48:54 -03:00
Davidson Gomes
68dcc1c86a adjusts in create instance 2024-05-23 11:56:52 -03:00
Davidson Gomes
f7dcab3736 Now in the manager, when logging in with the client's apikey, the listing only shows the instance corresponding to the provided apikey (only with MongoDB) 2024-05-23 11:31:45 -03:00
Davidson Gomes
2fcb476c50 Now in the manager, when logging in with the client's apikey, the listing only shows the instance corresponding to the provided apikey (only with MongoDB) 2024-05-23 11:30:47 -03:00
Antonio Milesi Bastos
44a7cd62c8 Merge pull request #1 from stack-app-br/releases/1.7.5
chore: build docker image to platforms: linux/amd64,linux/arm64
2024-05-21 11:56:53 -03:00
@milesibastos
f7f0f8251b chore: build docker image to platforms: linux/amd64,linux/arm64 2024-05-21 11:48:13 -03:00
Davidson Gomes
395b81a6ac Merge tag '1.7.5' into develop
v
2024-05-21 08:53:58 -03:00
Davidson Gomes
da06ed1185 Merge branch 'release/1.7.5' 2024-05-21 08:53:53 -03:00
Davidson Gomes
67afbd6a77 v1.7.5 2024-05-21 08:53:35 -03:00
Davidson Gomes
8fce53b4af fixed version whatsapp web for baileys 2024-05-21 08:38:08 -03:00
Davidson Gomes
65bba23519 update baileys version 2024-05-20 19:53:23 -03:00
Davidson Gomes
283b497788 downgrade baileys version 2024-05-20 19:39:56 -03:00
Davidson Gomes
8cc1b8daa8 Merge pull request #590 from Ckk3/fix-envvar-CLEAN_STORE_CLEANING_INTERVAL
Corrigindo variável inexistente do arquivo env.config.ts e substituindo pela correta CLEAN_STORE_CLEANING_INTERVAL
2024-05-09 14:15:09 -03:00
Davidson Gomes
9ccdb45a7a Merge pull request #587 from deivisonrpg/chatwoot-merge-contact-with-nine
feature(chatwoot): add merge_brazil_contacts function to solve nine digit in brazilian numbers
2024-05-09 14:12:31 -03:00
Davidson Gomes
f78d360c38 Merge pull request #586 from deivisonrpg/chatwoot-update-contact
refactor(chatwoot): optimize ChatwootService method for updating contact inform…
2024-05-09 14:11:07 -03:00
Ckk3
6144fbe856 changing CLEAN_STORE_CLEANING_TERMINAL to CLEAN_STORE_CLEANING_INTERVAL 2024-05-09 13:21:28 -03:00
Deivison Lincoln
8446be7646 fix(chatwoot): fix grupos 2024-05-08 16:47:21 -03:00
Deivison Lincoln
8875ab1e93 feat: add merge_brazil_contacts flag to instance controller 2024-05-08 15:26:56 -03:00
Deivison Lincoln
7c207a50e1 feat: add merge_brazil_contacts to swagger doc 2024-05-08 15:11:37 -03:00
Deivison Lincoln
dcc32479ff feature(chatwoot): add merge_brazil_contacts function to solve nine digit in brazilian numbers 2024-05-08 15:03:37 -03:00
Deivison Lincoln
09911c472d refactor: optimize ChatwootService method for updating contact information 2024-05-08 14:34:03 -03:00
Davidson Gomes
7a0149ee23 Merge pull request #571 from stack-app-br/main
chore: build docker image to platforms: linux/amd64, linux/arm64
2024-05-08 11:02:11 -03:00
Davidson Gomes
3ae8cf32b0 Merge pull request #584 from Al1st1c/fix/swagger-auth
Fix/swagger auth
2024-05-08 09:56:59 -03:00
Davidson Gomes
11cf947bbb Merge pull request #583 from deivisonrpg/chore-update-aws-sdk-v3
chore(aws sqs): update aws sdk v3
2024-05-08 09:56:24 -03:00
Davidson Gomes
60a20f61af Merge pull request #582 from deivisonrpg/hotfix-getOpenConversationByContact
fix(chatwoot): getOpenConversationByContact and init queries error
2024-05-08 09:55:08 -03:00
Davidson Gomes
e65c7b6bcf Merge pull request #577 from neanderdev/mark-chat-as-unread
feat: method to mark chat as unread
2024-05-08 09:54:37 -03:00
Al1stic
80c892aca3 fix env 2024-05-06 19:31:37 -03:00
dev2
39ee266598 fix: correcao swagger sem authkey 2024-05-06 19:28:57 -03:00
Deivison Lincoln
aa58d7744e chore: update AWS SDK dependency to version 3.569.0 2024-05-06 17:28:41 -03:00
Deivison Lincoln
ea3b0b3712 fix: optimize ChatwootService method for retrieving open conversations 2024-05-06 16:30:16 -03:00
Deivison Lincoln
d9d8707123 chore: update baileys to version 6.7.2 2024-05-06 16:29:37 -03:00
Neander de Souza
8e9a1e2ba5 feat: method to mark chat as unread 2024-05-06 09:08:36 -03:00
Davidson Gomes
e29b4865e8 update baileys 2024-05-01 19:20:40 -03:00
@milesibastos
4b60ca175d chore: build docker image to platforms: linux/amd64,linux/arm64 2024-04-30 17:48:35 -03:00
Davidson Gomes
2fbbc7b5a9 Merge tag '1.7.4' into develop
v
2024-04-28 09:47:24 -03:00
Davidson Gomes
633dbb82d3 Merge branch 'release/1.7.4' 2024-04-28 09:47:19 -03:00
Davidson Gomes
95907b3cea v 1.7.4 2024-04-28 09:47:06 -03:00
Davidson Gomes
14c210c771 fix: new version baileys 2024-04-28 09:41:22 -03:00
Davidson Gomes
00e7fcc46b changelog 2024-04-27 13:13:43 -03:00
Davidson Gomes
720efcbcbf Merge pull request #566 from judsonjuniorr/feat/cw-inbox-name
Chatwoot inbox name
2024-04-27 13:13:06 -03:00
Davidson Gomes
d95791cc18 changelog 2024-04-27 13:12:29 -03:00
Davidson Gomes
d45b7af3b6 fix: recovering messages 2024-04-27 13:11:33 -03:00
Davidson Gomes
0ad3acaf07 Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2024-04-24 18:55:49 -03:00
Davidson Gomes
e27818ecda adjusts in integration 2024-04-24 18:55:38 -03:00
Davidson Gomes
ff21cf4d4c Merge pull request #558 from jaison-x/pr2
feat(chatwoot): send private message on error message sent from chatwoot
2024-04-23 19:11:10 -03:00
Davidson Gomes
dc19c7fdec Merge pull request #555 from jaison-x/pr
fix(chatwoot): fix bug when chatwoot params reopen_conversation and conversation_pending are enabled
2024-04-23 19:10:57 -03:00
jaison-x
6c8ffd8ec6 feat(chatwoot): send private message on error message sent from chatwoot 2024-04-23 17:26:22 -03:00
jaison-x
96fdb210be feat(chatwoot): send private message on error message sent from chatwoot 2024-04-23 17:15:41 -03:00
jaison-x
84ad8e0d6e feat(chatwoot): send private message on error message sent from chatwoot 2024-04-23 15:09:28 -03:00
Davidson Gomes
53361682f4 Recovering messages 2024-04-23 08:01:59 -03:00
jaison-x
0ee243f284 fix(chatwoot): fix bug when chatwoot params reopen_conversation and conversation_pending are enabled 2024-04-22 13:41:06 -03:00
Judson Cairo
26ff0b634f Revert "Update current inbox if exists"
This reverts commit f44ab0f678.
2024-04-21 16:49:15 -03:00
Judson Cairo
f44ab0f678 Update current inbox if exists 2024-04-21 16:46:49 -03:00
Judson Cairo
1f128747bb Fix inbox name on cw config 2024-04-21 16:37:27 -03:00
Judson Cairo
3bf975d90f Set chatwoot custom inbox name 2024-04-21 16:33:20 -03:00
Davidson Gomes
92f8951be4 Merge tag '2.3.9' into develop
v
2024-04-18 21:24:30 -03:00
Davidson Gomes
e071f56767 Merge branch 'release/2.3.9' 2024-04-18 21:24:27 -03:00
Davidson Gomes
c85619efcf fix: Recovering messages lost with redis cache 2024-04-18 17:33:09 -03:00
Davidson Gomes
27f9ae1e56 fix: Recovering messages lost with redis cache 2024-04-18 17:31:10 -03:00
Davidson Gomes
7449102d95 fix: Recovering messages lost with redis cache 2024-04-18 17:31:05 -03:00
Davidson Gomes
234a2c71b5 Merge pull request #543 from PauloAK/history-set-message-status
Adicionado message.status ao webhook MESSAGES_SET
2024-04-18 16:05:44 -03:00
Davidson Gomes
4dd5533202 fix: Adjusts in proxy on fetchAgent 2024-04-18 12:54:00 -03:00
Davidson Gomes
a4d1740754 fix: Adjusts in proxy on fetchAgent 2024-04-18 12:53:55 -03:00
Davidson Gomes
1a2ea1c38a Merge tag '1.7.3' into develop
* Revert fix audio encoding
* Recovering messages lost with redis cache
* Adjusts in redis for save instances
* Adjusts in proxy
* Revert pull request #523
* Added instance name on logs
* Added support for Spanish
* Fix error: invalid operator. The allowed operators for identifier are equal_to,not_equal_to in chatwoot
2024-04-18 12:12:40 -03:00
Davidson Gomes
371ec9b8d5 Merge branch 'release/1.7.3' 2024-04-18 12:12:27 -03:00
Davidson Gomes
5e288d57ea changelog 2024-04-18 12:12:11 -03:00
Davidson Gomes
61bd5b3484 fix: Adjusts in redis for save instances 2024-04-18 10:39:24 -03:00
Paulo Kramer
bb974e10f5 Added message status to the messaging-history.set 2024-04-18 01:34:52 -03:00
Davidson Gomes
4ed1335f89 changelog 2024-04-17 19:58:31 -03:00
Davidson Gomes
4274c7afdb Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2024-04-17 18:48:11 -03:00
Davidson Gomes
27f67142c8 changelog 2024-04-17 18:46:50 -03:00
Davidson Gomes
ac8aba6ba4 Merge pull request #542 from nestordavalos/develop
 feat: add support for Spanish
2024-04-17 18:45:38 -03:00
Davidson Gomes
07ff61c070 changelog 2024-04-17 18:25:35 -03:00
nestordavalos
3645ac6704 Merge branch 'develop' of https://github.com/EvolutionAPI/evolution-api into develop 2024-04-17 17:20:42 -04:00
nestordavalos
9764719245 feat: add support for Spanish 2024-04-17 17:20:37 -04:00
Davidson Gomes
b3aeed7fc1 fix: retry messages 2024-04-17 18:14:11 -03:00
Davidson Gomes
5ae5d8546e fix: retry messages 2024-04-17 18:10:43 -03:00
Davidson Gomes
d190d8b1af Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2024-04-17 12:43:55 -03:00
Davidson Gomes
c45538684d fix: fix audio encoding 2024-04-17 12:43:49 -03:00
Davidson Gomes
575e7bf4c4 Merge pull request #539 from jaison-x/pr2
fix: this.localProxy.proxy can be undefined
2024-04-17 12:08:54 -03:00
jaison-x
a4338d8472 fix: this.localProxy.proxy can be undefined 2024-04-17 11:03:55 -03:00
Davidson Gomes
7d542cf115 Merge pull request #538 from jaison-x/pr
fix(chatwoot): error: invalid operator. The allowed operators for identifier are [equal_to,not_equal_to]
2024-04-17 11:03:35 -03:00
jaison-x
9c9a542bbe fix(chatwoot): error: invalid operator. The allowed operators for identifier are [equal_to,not_equal_to]
In chatwoot version 3.8 we can't use the filter operator 'contains' for the field 'identifier'
2024-04-17 10:50:25 -03:00
Davidson Gomes
b6c56551bc fix: Revert pull request #523 2024-04-16 19:13:25 -03:00
Davidson Gomes
1379228196 fix: Revert pull request #523 2024-04-16 19:12:03 -03:00
Davidson Gomes
d6e19b9273 adjusts in v1.7.3 2024-04-16 19:08:44 -03:00
Davidson Gomes
5e0e4051dc Merge tag '1.7.2' into develop
* Mobile connection via sms (test)

* Adjusts in redis
* Send global event in websocket
* Adjusts in proxy
* Fix audio encoding
* Fix conversation read on chatwoot version 3.7
* Fix when receiving/sending messages from whatsapp desktop with ephemeral messages enabled
* Changed returned sessions on typebot status change
* Reorganization of files and folders
2024-04-12 17:32:30 -03:00
64 changed files with 2147 additions and 846 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,38 +0,0 @@
---
name: "[EN] Bug report"
about: Create a report to help us improve
title: "[EN][BUG]"
labels: bug
assignees: ''
---
### Title: [Brief Description of the Bug]
#### Description:
Describe in detail the problem you encountered. Include any relevant context that may help understand the origin of the bug.
#### Steps to Reproduce:
1. List the steps necessary to reproduce the problem.
2. Try to be as specific as possible.
3. If the problem occurs in a specific scenario, describe it here.
#### Expected Behavior:
Describe what you expected to happen when following the steps above.
#### Current Behavior:
Explain what actually happens when you follow the steps above.
#### Screenshots/Videos:
If possible, add screenshots or videos illustrating the problem. This can be extremely helpful in understanding the issue.
#### Environment:
- **Server:** [e.g., Ubuntu 18.04]
- **API Version:** [e.g., 1.5.4]
- **Other Hardware/Software Specifications:** [e.g., CPU, GPU]
#### Submitting Logs:
Please attach logs that may be related to the problem. If the logs contain sensitive information, consider sending them privately to one of the project maintainers.
#### Additional Notes:
Include here any other information that you think might be useful in understanding or resolving the bug.

View File

@@ -0,0 +1,107 @@
name: Bug Report
description: Create a report to help us improve.
labels:
- bug
- en
# - help wanted
# Automatically assign the issue to:
# assignees: DavidsonGomes
body:
- type: checkboxes
id: terms
attributes:
label: Welcome!
description: |
The issue tracker is only for reporting bugs and feature requests.
For user-related support questions, please visit:
- [Discord](https://evolution-api.com/discord)
- [WhatsApp Group](https://evolution-api.com/whatsapp)
<br/>
**DO NOT OPEN AN ISSUE FOR GENERAL SUPPORT QUESTIONS.**
options:
- label: Yes, I have searched for similar issues on [GitHub](https://github.com/EvolutionAPI/evolution-api/issues) and found none.
required: true
- type: textarea
attributes:
label: What did you do?
description: |
How to write a good bug report?
- Respect the issue template as much as possible.
- The title should be short and descriptive.
- Explain the conditions that led you to report this issue: the context.
- The context should lead to something, an idea or problem you are facing.
- Be clear and concise.
- Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown)
placeholder: Describe the problem you encountered in detail.
validations:
required: true
- type: textarea
attributes:
label: What did you expect?
placeholder: Describe what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: What did you observe instead of what you expected?
placeholder: Explain what actually happens when you follow the steps above.
validations:
required: true
- type: textarea
attributes:
label: Screenshots/Videos
placeholder: |
If possible, add screenshots or videos that illustrate the problem. This can be extremely helpful in understanding the issue better.
- type: textarea
attributes:
label: Which version of the API are you using?
description: |
Enter the version number found in the `version` property of the **package.json**.
placeholder: Paste the version here.
validations:
required: true
- type: dropdown
id: select
attributes:
label: What is your environment?
options:
- Windows
- Linux
- Mac
- Docker
- Other
validations:
required: true
- type: textarea
attributes:
label: Other environment specifications
placeholder: 'Hardware/Software: [e.g., CPU, GPU]'
validations:
required: false
- type: textarea
attributes:
label: If applicable, paste the log output
description: |
Please attach any logs that might be related to the issue.
If the logs contain sensitive information, consider sending them privately to one of the project maintainers.
placeholder: Paste the application log here.
validations:
required: false
- type: textarea
attributes:
label: Additional Notes
description: Include any other information you think might be useful to understand or resolve the bug.
validations:
required: false

View File

@@ -1,28 +0,0 @@
---
name: "[EN] Feature request"
about: Suggest an idea for the API
title: "[EN][FEAT]"
labels: enhancement
assignees: ''
---
### Title: [Brief Description of Feature Request]
#### Detailed Description:
Clearly and in detail, describe the functionality you wish to be implemented. Explain how this fits into the context of the project.
#### Rationale:
Explain why this functionality would be useful for the project. This helps in understanding the importance and priority of the request.
#### Usage Examples:
Provide specific examples of how this feature could be used. This can include scenarios or use cases where the feature would be particularly beneficial.
#### Possible Implementations:
If you have ideas on how this feature might be implemented, please share them here. This is not mandatory but can be helpful for the development team.
#### Impact on the Project:
Discuss how this new feature could impact other parts of the project, if applicable.
#### Additional Notes:
Any other information you believe is relevant to your request.

View File

@@ -0,0 +1,86 @@
name: Feature Request
description: Suggest ideas for the project.
labels:
- enhancement
- en
# Automatically assign the issue to:
# assignees: DavidsonGomes
body:
- type: checkboxes
id: terms
attributes:
label: Welcome!
description: '**DO NOT OPEN FOR GENERAL SUPPORT QUESTIONS.**'
options:
- label: Yes, I have searched for similar requests on [GitHub](https://github.com/code-chat-br/whatsapp-api/issues) and found none.
required: true
- type: dropdown
attributes:
label: What type of feature?
description: |
How to write a good feature request?
- Respect the issue template as much as possible.
- The title should be short and descriptive.
- Explain the conditions that led you to suggest this feature: the context.
- The context should lead to something, an idea or problem you are facing.
- Be clear and concise.
- Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown)
options:
- Integration
- Functionality
- Endpoint
- Database adjustment
- Other
validations:
required: true
- type: textarea
attributes:
label: What is the motivation for the request?
description: |
What problem does the feature seek to solve?
Clearly and in detail describe the functionality you want to be implemented.
Explain how it fits into the context of the project.
placeholder: Detailed description
validations:
required: true
- type: textarea
attributes:
label: Usage Examples
description: |
Provide specific examples of how this functionality could be used.
This can include scenarios or use cases where the feature would be particularly beneficial.
placeholder: text - image - video - flowcharts
validations:
required: false
- type: textarea
attributes:
label: How should the feature be developed?
description: |
Should it be inserted directly into the code?
Should it be built as a different application that acts as an extension of the API?
For example: a `worker`?
Discuss how this new functionality could impact other parts of the project, if applicable.
---
If you have ideas on how this functionality could be implemented, please share them here.
This is not mandatory, but it can be helpful for the development team.
placeholder: Insert feature ideas here
validations:
required: false
- type: textarea
attributes:
label: Additional Notes
description: Any other information you believe is relevant to your request.
placeholder: Insert your observations here.
validations:
required: false

View File

@@ -1,38 +0,0 @@
---
name: "[PT] Reportar bug"
about: Reportar um problema
title: "[PT][BUG]"
labels: bug
assignees: ''
---
### Título: [Breve Descrição do Bug]
#### Descrição:
Descreva detalhadamente o problema que você encontrou. Inclua qualquer contexto relevante que possa ajudar a entender a origem do bug.
#### Passos para Reproduzir:
1. Liste os passos necessários para reproduzir o problema.
2. Tente ser o mais específico possível.
3. Se o problema ocorrer em um cenário específico, descreva-o aqui.
#### Comportamento Esperado:
Descreva o que você esperava que acontecesse quando seguisse os passos acima.
#### Comportamento Atual:
Explique o que realmente acontece quando você segue os passos acima.
#### Capturas de Tela/Vídeos:
Se possível, adicione capturas de tela ou vídeos que ilustrem o problema. Isso pode ser extremamente útil para entender o problema.
#### Ambiente:
- **Servidor:** [ex: Ubuntu 18.04]
- **Versão da API:** [ex: 1.5.4]
- **Outras Especificações de Hardware/Software:** [ex: CPU, GPU]
#### Envio de Logs:
Por favor, anexe os logs que possam estar relacionados ao problema. Se os logs contiverem informações sensíveis, considere enviá-los de forma privada para um dos mantenedores do projeto.
#### Notas Adicionais:
Inclua aqui qualquer outra informação que você ache que possa ser útil para entender ou resolver o bug.

View File

@@ -0,0 +1,108 @@
name: Relatório de bug
description: Crie um relatório para nos ajudar a melhorar.
labels:
- bug
- pt-br
# - help wanted
# Atrubuir automaticamente a issue a:
# assignees: DavidsonGomes
body:
- type: checkboxes
id: termos
attributes:
label: Bem-vido!
description: |
O rastreador de problemas é apenas para relatar bugs e solicitações de recursos.
Para perguntas de suporte relacionadas ao usuário final, acesse:
- [Discord](https://evolution-api.com/discord)
- [Grupo do WhatsApp](https://evolution-api.com/whatsapp)
<br/>
**NÃO ABRA UM PROBLEMA PARA PERGUNTAS GERAIS DE SUPORTE.**
options:
- label: Sim, pesquisei problemas semelhantes no [GitHub](https://github.com/EvolutionAPI/evolution-api/issues) e não encontrei nenhum.
required: true
- type: textarea
attributes:
label: O que você fez?
description: |
Como escrever um bom relatório de bug?
- Respeite o modelo de problema tanto quanto possível.
- O título deve ser curto e descritivo.
- Explique as condições que o levaram a reportar este problema: o contexto.
- O contexto deve levar a algo, ideia ou problema que você está enfrentando.
- Seja claro e conciso.
- Formate suas mensagens para ajudar o leitor a se concentrar no que importa e entender a estrutura da sua mensagem, use [sintaxe Markdown](https://help.github.com/articles/github-flavored-markdown)
placeholder: Descreva detalhadamente o problema que você encontrou.
validations:
required: true
- type: textarea
attributes:
label: O que você esperava?
placeholder: Descreva o que você esperava que acontecesse.
validations:
required: true
- type: textarea
attributes:
label: O que vc observou ao invés do que esperava?
placeholder: Explique o que realmente acontece quando você segue os passos acima.
validations:
required: true
- type: textarea
attributes:
label: Capturas de Tela/Vídeos
placeholder: |
Se possível, adicione capturas de tela ou vídeos que ilustrem o problema. Isso pode ser extremamente útil para entender melhor o problema.
- type: textarea
attributes:
label: Qual versão da API você está usando?
description: |
Insira o número da sua versão que está na propriedade `version` do **package.json**.
placeholder: Cole aqui a versão.
validations:
required: true
- type: dropdown
id: select
attributes:
label: Qual é o seu ambiente?
options:
- Windows
- Linux
- Mac
- Docker
- Outro
validations:
required: true
- type: textarea
attributes:
label: Outras expecificações do ambiente
placeholder: 'Hardware/Software: [ex: CPU, GPU]'
validations:
required: false
- type: textarea
attributes:
label: Se aplicável, cole a saída do log
description: |
Por favor, anexe os logs que possam estar relacionados ao problema.
Se os logs contiverem informações sensíveis, considere enviá-los de forma privada para um dos mantenedores do projeto.
placeholder: Cole aqui o log da aplicação
validations:
required: false
- type: textarea
attributes:
label: Notas Adicionais
description: Inclua aqui qualquer outra informação que você ache que possa ser útil para entender ou resolver o bug.
validations:
required: false

View File

@@ -1,28 +0,0 @@
---
name: "[PT] Solicitar recurso"
about: Sugira novos recursos para a API
title: "[PT][FEAT]"
labels: enhancement
assignees: ''
---
### Título: [Breve Descrição da Solicitação de Recurso]
#### Descrição Detalhada:
Descreva claramente e em detalhes a funcionalidade que você deseja que seja implementada. Explique como isso se encaixa no contexto do projeto.
#### Racional:
Explique por que essa funcionalidade seria útil para o projeto. Isso ajuda a entender a importância e a prioridade da solicitação.
#### Exemplos de Uso:
Forneça exemplos específicos de como essa funcionalidade poderia ser utilizada. Isso pode incluir cenários ou casos de uso onde a funcionalidade seria particularmente benéfica.
#### Possíveis Implementações:
Se você tem ideias sobre como essa funcionalidade pode ser implementada, por favor, compartilhe-as aqui. Isso não é obrigatório, mas pode ser útil para a equipe de desenvolvimento.
#### Impacto no Projeto:
Discuta como essa nova funcionalidade poderia impactar outras partes do projeto, se aplicável.
#### Notas Adicionais:
Qualquer outra informação que você acredita ser relevante para a sua solicitação.

View File

@@ -0,0 +1,86 @@
name: Solicitação de recursos
description: Sugira ideias para o projeto.
labels:
- enhancement
- pt-br
# Automatically assign the issue to:
# assignees: DavidsonGomes
body:
- type: checkboxes
id: termos
attributes:
label: Bem-vindo!
description: '**NÃO ABRA PARA PERGUNTAS GERAIS DE SUPORTE.**'
options:
- label: Sim, pesquisei solicitações semelhantes no [GitHub](https://github.com/code-chat-br/whatsapp-api/issues) e não encontrei nenhum.
required: true
- type: dropdown
attributes:
label: Qual tipo de recurso?
description: |
Como escrever uma boa solicitação de bug?
- Respeite o modelo de problema tanto quanto possível.
- O título deve ser curto e descritivo.
- Explique as condições que o levaram a reportar este problema: o contexto.
- O contexto deve levar a algo, ideia ou problema que você está enfrentando.
- Seja claro e conciso.
- Formate suas mensagens para ajudar o leitor a se concentrar no que importa e entender a estrutura da sua mensagem, use [sintaxe Markdown](https://help.github.com/articles/github-flavored-markdown)
options:
- Integração
- Funcionalidade
- Endpoint
- Ajuste de banco de dados
- Outro
validations:
required: true
- type: textarea
attributes:
label: Qual a motivação para a solicitação?
description: |
Qual problema o recurso busca resolver?
Descreva claramente e em detalhes a funcionalidade que você deseja que seja implementada.
Explique como isso se encaixa no contexto do projeto.
placeholder: Descrição detalhada
validations:
required: true
- type: textarea
attributes:
label: Exemplos de Uso
description: |
Forneça exemplos específicos de como essa funcionalidade poderia ser utilizada.
Isso pode incluir cenários ou casos de uso onde a funcionalidade seria particularmente benéfica.
placeholder: texto - imagem - video - fluxogramas
validations:
required: false
- type: textarea
attributes:
label: Como o recurso deve ser desenvolvido?
description: |
Deve ser inserido diretamente no código?
Deve ser construído uma aplicação diferente que atuará como uma extenção da api?
Por exemplo: um `worker`?
Discuta como essa nova funcionalidade poderia impactar outras partes do projeto, se aplicável.
---
Se você tem ideias sobre como essa funcionalidade pode ser implementada, por favor, compartilhe-as aqui.
Isso não é obrigatório, mas pode ser útil para a equipe de desenvolvimento.
placeholder: Insira ideias para o recurso
validations:
required: false
- type: textarea
attributes:
label: Notas Adicionais
description: Qualquer outra informação que você acredita ser relevante para a sua solicitação.
placeholder: Insira aqui as sua observções.
validations:
required: false

View File

@@ -2,63 +2,47 @@ name: Build Docker image
on: on:
push: push:
tags: ['v*'] tags:
- "*.*.*"
jobs: jobs:
build-amd: build_deploy:
name: Build and Deploy
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps: steps:
- name: Check out the repo - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Extract existing image metadata - name: Docker meta
id: image-meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: atendai/evolution-api images: atendai/evolution-api
tags: type=semver,pattern=v{{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub - name: Login to GitHub Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push AMD image - name: Build and push
uses: docker/build-push-action@v4 id: docker_build
uses: docker/build-push-action@v5
with: with:
context: . platforms: linux/amd64,linux/arm64
labels: ${{ steps.image-meta.outputs.labels }}
platforms: linux/amd64
push: true push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-arm: - name: Image digest
runs-on: buildjet-4vcpu-ubuntu-2204-arm run: echo ${{ steps.docker_build.outputs.digest }}
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Extract existing image metadata
id: image-meta
uses: docker/metadata-action@v4
with:
images: atendai/evolution-api
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push ARM image
uses: docker/build-push-action@v4
with:
context: .
labels: ${{ steps.image-meta.outputs.labels }}
platforms: linux/arm64
push: true

View File

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

View File

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

View File

@@ -1,3 +1,62 @@
# 1.8.1 (2024-06-08 21:32)
### Feature
* New method of saving sessions to a file using worker, made in partnership with [codechat](https://github.com/code-chat-br/whatsapp-api)
### Fixed
* Correction of variables breaking lines in typebot
# 1.8.0 (2024-05-27 16:10)
### Feature
* Now in the manager, when logging in with the client's apikey, the listing only shows the instance corresponding to the provided apikey (only with MongoDB)
* New global mode for rabbitmq events
* Build in docker for linux/amd64, linux/arm64 platforms
### Fixed
* Correction in message formatting when generated by AI as markdown in typebot
* Security fix in fetch instance with client key when not connected to mongodb
# 1.7.5 (2024-05-21 08:50)
### Fixed
* Add merge_brazil_contacts function to solve nine digit in brazilian numbers
* Optimize ChatwootService method for updating contact
* Fix swagger auth
* Update aws sdk v3
* Fix getOpenConversationByContact and init queries error
* Method to mark chat as unread
* Added environment variable to manually select the WhatsApp web version for the baileys lib (optional)
# 1.7.4 (2024-04-28 09:46)
### Fixed
* Adjusts in proxy on fetchAgent
* Recovering messages lost with redis cache
* Log when init redis cache service
* Recovering messages lost with redis cache
* Chatwoot inbox name
* Update Baileys version
# 1.7.3 (2024-04-18 12:07)
### Fixed
* Revert fix audio encoding
* Recovering messages lost with redis cache
* Adjusts in redis for save instances
* Adjusts in proxy
* Revert pull request #523
* Added instance name on logs
* Added support for Spanish
* Fix error: invalid operator. The allowed operators for identifier are equal_to,not_equal_to in chatwoot
# 1.7.2 (2024-04-12 17:31) # 1.7.2 (2024-04-12 17:31)
### Feature ### Feature

View File

@@ -43,14 +43,34 @@ DATABASE_SAVE_MESSAGE_UPDATE=false
DATABASE_SAVE_DATA_CONTACTS=false DATABASE_SAVE_DATA_CONTACTS=false
DATABASE_SAVE_DATA_CHATS=false DATABASE_SAVE_DATA_CHATS=false
REDIS_ENABLED=false
REDIS_URI=redis://redis:6379
REDIS_PREFIX_KEY=evdocker
RABBITMQ_ENABLED=false RABBITMQ_ENABLED=false
RABBITMQ_RABBITMQ_MODE=global
RABBITMQ_EXCHANGE_NAME=evolution_exchange
RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672 RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672
RABBITMQ_EXCHANGE_NAME=evolution_exchange
RABBITMQ_GLOBAL_ENABLED=false
RABBITMQ_EVENTS_APPLICATION_STARTUP=false
RABBITMQ_EVENTS_QRCODE_UPDATED=true
RABBITMQ_EVENTS_MESSAGES_SET=true
RABBITMQ_EVENTS_MESSAGES_UPSERT=true
RABBITMQ_EVENTS_MESSAGES_UPDATE=true
RABBITMQ_EVENTS_MESSAGES_DELETE=true
RABBITMQ_EVENTS_SEND_MESSAGE=true
RABBITMQ_EVENTS_CONTACTS_SET=true
RABBITMQ_EVENTS_CONTACTS_UPSERT=true
RABBITMQ_EVENTS_CONTACTS_UPDATE=true
RABBITMQ_EVENTS_PRESENCE_UPDATE=true
RABBITMQ_EVENTS_CHATS_SET=true
RABBITMQ_EVENTS_CHATS_UPSERT=true
RABBITMQ_EVENTS_CHATS_UPDATE=true
RABBITMQ_EVENTS_CHATS_DELETE=true
RABBITMQ_EVENTS_GROUPS_UPSERT=true
RABBITMQ_EVENTS_GROUPS_UPDATE=true
RABBITMQ_EVENTS_GROUP_PARTICIPANTS_UPDATE=true
RABBITMQ_EVENTS_CONNECTION_UPDATE=true
RABBITMQ_EVENTS_LABELS_EDIT=true
RABBITMQ_EVENTS_LABELS_ASSOCIATION=true
RABBITMQ_EVENTS_CALL=true
RABBITMQ_EVENTS_TYPEBOT_START=false
RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS=false
WEBSOCKET_ENABLED=false WEBSOCKET_ENABLED=false
WEBSOCKET_GLOBAL_EVENTS=false WEBSOCKET_GLOBAL_EVENTS=false
@@ -73,7 +93,7 @@ WEBHOOK_GLOBAL_URL=''
WEBHOOK_GLOBAL_ENABLED=false 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 # 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 WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false
## Set the events you want to hear ## Set the events you want to hear
WEBHOOK_EVENTS_APPLICATION_STARTUP=false WEBHOOK_EVENTS_APPLICATION_STARTUP=false
WEBHOOK_EVENTS_QRCODE_UPDATED=true WEBHOOK_EVENTS_QRCODE_UPDATED=true
WEBHOOK_EVENTS_MESSAGES_SET=true WEBHOOK_EVENTS_MESSAGES_SET=true
@@ -114,7 +134,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
# Set qrcode display limit # Set qrcode display limit
QRCODE_LIMIT=30 QRCODE_LIMIT=30
QRCODE_COLOR=#198754 QRCODE_COLOR='#198754'
# old | latest # old | latest
TYPEBOT_API_VERSION=latest TYPEBOT_API_VERSION=latest
@@ -126,9 +146,17 @@ CHATWOOT_MESSAGE_DELETE=false # false | true
# If you leave this option as true, when sending a message in Chatwoot, the client's last message will be marked as read on WhatsApp. # If you leave this option as true, when sending a message in Chatwoot, the client's last message will be marked as read on WhatsApp.
CHATWOOT_MESSAGE_READ=false # false | true CHATWOOT_MESSAGE_READ=false # false | true
# This db connection is used to import messages from whatsapp to chatwoot database # This db connection is used to import messages from whatsapp to chatwoot database
CHATWOOT_IMPORT_DATABASE_CONNECTION_URI=postgres://user:password@hostname:port/dbname CHATWOOT_IMPORT_DATABASE_CONNECTION_URI=postgres://user:password@hostname:port/dbname?sslmode=disable
CHATWOOT_IMPORT_DATABASE_PLACEHOLDER_MEDIA_MESSAGE=true CHATWOOT_IMPORT_DATABASE_PLACEHOLDER_MEDIA_MESSAGE=true
CACHE_REDIS_ENABLED=false
CACHE_REDIS_URI=redis://redis:6379
CACHE_REDIS_PREFIX_KEY=evolution
CACHE_REDIS_TTL=604800
CACHE_REDIS_SAVE_INSTANCES=false
CACHE_LOCAL_ENABLED=false
CACHE_LOCAL_TTL=604800
# Defines an authentication type for the api # Defines an authentication type for the api
# We recommend using the apikey because it will allow you to use a custom token, # We recommend using the apikey because it will allow you to use a custom token,
# if you use jwt, a random token will be generated and may be expired and you will have to generate a new token # if you use jwt, a random token will be generated and may be expired and you will have to generate a new token
@@ -143,4 +171,4 @@ AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true
AUTHENTICATION_JWT_EXPIRIN_IN=0 AUTHENTICATION_JWT_EXPIRIN_IN=0
AUTHENTICATION_JWT_SECRET='L=0YWt]b2w[WF>#>:&E`' AUTHENTICATION_JWT_SECRET='L=0YWt]b2w[WF>#>:&E`'
LANGUAGE=en # pt-BR, en LANGUAGE=en # pt-BR, en

View File

@@ -33,7 +33,10 @@ CLEAN_STORE_CHATS=true
# Permanent data storage # Permanent data storage
DATABASE_ENABLED=true DATABASE_ENABLED=true
DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin &
readPreference=primary &
ssl=false &
directConnection=true
DATABASE_CONNECTION_DB_PREFIX_NAME=evolution DATABASE_CONNECTION_DB_PREFIX_NAME=evolution
# Choose the data you want to save in the application's database or store # Choose the data you want to save in the application's database or store
@@ -43,10 +46,6 @@ DATABASE_SAVE_MESSAGE_UPDATE=false
DATABASE_SAVE_DATA_CONTACTS=false DATABASE_SAVE_DATA_CONTACTS=false
DATABASE_SAVE_DATA_CHATS=false DATABASE_SAVE_DATA_CHATS=false
REDIS_ENABLED=true
REDIS_URI=redis://redis:6379
REDIS_PREFIX_KEY=evolution
# Global Webhook Settings # Global Webhook Settings
# Each instance's Webhook URL and events will be requested at the time it is created # Each instance's Webhook URL and events will be requested at the time it is created
## Define a global webhook that will listen for enabled events from all instances ## Define a global webhook that will listen for enabled events from all instances
@@ -54,7 +53,7 @@ WEBHOOK_GLOBAL_URL=''
WEBHOOK_GLOBAL_ENABLED=false 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 # 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 WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false
## Set the events you want to hear ## Set the events you want to hear
WEBHOOK_EVENTS_APPLICATION_STARTUP=false WEBHOOK_EVENTS_APPLICATION_STARTUP=false
WEBHOOK_EVENTS_QRCODE_UPDATED=true WEBHOOK_EVENTS_QRCODE_UPDATED=true
WEBHOOK_EVENTS_MESSAGES_SET=true WEBHOOK_EVENTS_MESSAGES_SET=true
@@ -87,6 +86,14 @@ CONFIG_SESSION_PHONE_NAME=chrome
# Set qrcode display limit # Set qrcode display limit
QRCODE_LIMIT=30 QRCODE_LIMIT=30
CACHE_REDIS_ENABLED=false
CACHE_REDIS_URI=redis://redis:6379
CACHE_REDIS_PREFIX_KEY=evolution
CACHE_REDIS_TTL=604800
CACHE_REDIS_SAVE_INSTANCES=false
CACHE_LOCAL_ENABLED=false
CACHE_LOCAL_TTL=604800
# Defines an authentication type for the api # Defines an authentication type for the api
# We recommend using the apikey because it will allow you to use a custom token, # We recommend using the apikey because it will allow you to use a custom token,
# if you use jwt, a random token will be generated and may be expired and you will have to generate a new token # if you use jwt, a random token will be generated and may be expired and you will have to generate a new token
@@ -109,4 +116,4 @@ AUTHENTICATION_INSTANCE_NAME=evolution
AUTHENTICATION_INSTANCE_WEBHOOK_URL='' AUTHENTICATION_INSTANCE_WEBHOOK_URL=''
AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=1 AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=1
AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456 AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456
AUTHENTICATION_INSTANCE_CHATWOOT_URL='' AUTHENTICATION_INSTANCE_CHATWOOT_URL=''

View File

@@ -1,6 +1,6 @@
FROM node:20.7.0-alpine AS builder FROM node:20.7.0-alpine AS builder
LABEL version="1.7.2" description="Api to control whatsapp features through http requests." LABEL version="1.8.0" description="Api to control whatsapp features through http requests."
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
LABEL contact="contato@agenciadgcode.com" LABEL contact="contato@agenciadgcode.com"
@@ -58,14 +58,36 @@ ENV DATABASE_SAVE_MESSAGE_UPDATE=false
ENV DATABASE_SAVE_DATA_CONTACTS=false ENV DATABASE_SAVE_DATA_CONTACTS=false
ENV DATABASE_SAVE_DATA_CHATS=false ENV DATABASE_SAVE_DATA_CHATS=false
ENV REDIS_ENABLED=false
ENV REDIS_URI=redis://redis:6379
ENV REDIS_PREFIX_KEY=evolution
ENV RABBITMQ_ENABLED=false ENV RABBITMQ_ENABLED=false
ENV RABBITMQ_MODE=global
ENV RABBITMQ_EXCHANGE_NAME=evolution_exchange
ENV RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672 ENV RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672
ENV RABBITMQ_EXCHANGE_NAME=evolution_exchange
ENV RABBITMQ_GLOBAL_ENABLED=false
ENV RABBITMQ_EVENTS_APPLICATION_STARTUP=false
ENV RABBITMQ_EVENTS_INSTANCE_CREATE=false
ENV RABBITMQ_EVENTS_INSTANCE_DELETE=false
ENV RABBITMQ_EVENTS_QRCODE_UPDATED=true
ENV RABBITMQ_EVENTS_MESSAGES_SET=true
ENV RABBITMQ_EVENTS_MESSAGES_UPSERT=true
ENV RABBITMQ_EVENTS_MESSAGES_UPDATE=true
ENV RABBITMQ_EVENTS_MESSAGES_DELETE=true
ENV RABBITMQ_EVENTS_SEND_MESSAGE=true
ENV RABBITMQ_EVENTS_CONTACTS_SET=true
ENV RABBITMQ_EVENTS_CONTACTS_UPSERT=true
ENV RABBITMQ_EVENTS_CONTACTS_UPDATE=true
ENV RABBITMQ_EVENTS_PRESENCE_UPDATE=true
ENV RABBITMQ_EVENTS_CHATS_SET=true
ENV RABBITMQ_EVENTS_CHATS_UPSERT=true
ENV RABBITMQ_EVENTS_CHATS_UPDATE=true
ENV RABBITMQ_EVENTS_CHATS_DELETE=true
ENV RABBITMQ_EVENTS_GROUPS_UPSERT=true
ENV RABBITMQ_EVENTS_GROUPS_UPDATE=true
ENV RABBITMQ_EVENTS_GROUP_PARTICIPANTS_UPDATE=true
ENV RABBITMQ_EVENTS_CONNECTION_UPDATE=true
ENV RABBITMQ_EVENTS_LABELS_EDIT=true
ENV RABBITMQ_EVENTS_LABELS_ASSOCIATION=true
ENV RABBITMQ_EVENTS_CALL=true
ENV RABBITMQ_EVENTS_TYPEBOT_START=false
ENV RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS=false
ENV WEBSOCKET_ENABLED=false ENV WEBSOCKET_ENABLED=false
ENV WEBSOCKET_GLOBAL_EVENTS=false ENV WEBSOCKET_GLOBAL_EVENTS=false
@@ -129,6 +151,14 @@ ENV QRCODE_COLOR=#198754
ENV TYPEBOT_API_VERSION=latest ENV TYPEBOT_API_VERSION=latest
ENV CACHE_REDIS_ENABLED=false
ENV CACHE_REDIS_URI=redis://redis:6379
ENV CACHE_REDIS_PREFIX_KEY=evolution
ENV CACHE_REDIS_TTL=604800
ENV CACHE_REDIS_SAVE_INSTANCES=false
ENV CACHE_LOCAL_ENABLED=false
ENV CACHE_LOCAL_TTL=604800
ENV AUTHENTICATION_TYPE=apikey ENV AUTHENTICATION_TYPE=apikey
ENV AUTHENTICATION_API_KEY=B6D711FCDE4D4FD5936544120E713976 ENV AUTHENTICATION_API_KEY=B6D711FCDE4D4FD5936544120E713976

View File

@@ -1,6 +1,6 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "1.7.2", "version": "1.8.1",
"description": "Rest api for communication with WhatsApp", "description": "Rest api for communication with WhatsApp",
"main": "./dist/src/main.js", "main": "./dist/src/main.js",
"scripts": { "scripts": {
@@ -46,10 +46,10 @@
"@figuro/chatwoot-sdk": "^1.1.16", "@figuro/chatwoot-sdk": "^1.1.16",
"@hapi/boom": "^10.0.1", "@hapi/boom": "^10.0.1",
"@sentry/node": "^7.59.2", "@sentry/node": "^7.59.2",
"@whiskeysockets/baileys": "github:AtendAI/Baileys",
"amqplib": "^0.10.3", "amqplib": "^0.10.3",
"aws-sdk": "^2.1499.0", "@aws-sdk/client-sqs": "^3.569.0",
"axios": "^1.6.5", "axios": "^1.6.5",
"@whiskeysockets/baileys": "6.7.5",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"compression": "^1.7.4", "compression": "^1.7.4",
"cors": "^2.8.5", "cors": "^2.8.5",

View File

@@ -1,13 +1,19 @@
export interface ICache { export interface ICache {
get(key: string): Promise<any>; get(key: string): Promise<any>;
hGet(key: string, field: string): Promise<any>;
set(key: string, value: any, ttl?: number): void; set(key: string, value: any, ttl?: number): void;
hSet(key: string, field: string, value: any): Promise<void>;
has(key: string): Promise<boolean>; has(key: string): Promise<boolean>;
keys(appendCriteria?: string): Promise<string[]>; keys(appendCriteria?: string): Promise<string[]>;
delete(key: string | string[]): Promise<number>; delete(key: string | string[]): Promise<number>;
hDelete(key: string, field: string): Promise<any>;
deleteAll(appendCriteria?: string): Promise<number>; deleteAll(appendCriteria?: string): Promise<number>;
} }

View File

@@ -4,6 +4,7 @@ import {
BlockUserDto, BlockUserDto,
DeleteMessage, DeleteMessage,
getBase64FromMediaMessageDto, getBase64FromMediaMessageDto,
MarkChatUnreadDto,
NumberDto, NumberDto,
PrivacySettingDto, PrivacySettingDto,
ProfileNameDto, ProfileNameDto,
@@ -40,6 +41,11 @@ export class ChatController {
return await this.waMonitor.waInstances[instanceName].archiveChat(data); return await this.waMonitor.waInstances[instanceName].archiveChat(data);
} }
public async markChatUnread({ instanceName }: InstanceDto, data: MarkChatUnreadDto) {
logger.verbose('requested markChatUnread from ' + instanceName + ' instance');
return await this.waMonitor.waInstances[instanceName].markChatUnread(data);
}
public async deleteMessage({ instanceName }: InstanceDto, data: DeleteMessage) { public async deleteMessage({ instanceName }: InstanceDto, data: DeleteMessage) {
logger.verbose('requested deleteMessage from ' + instanceName + ' instance'); logger.verbose('requested deleteMessage from ' + instanceName + ' instance');
return await this.waMonitor.waInstances[instanceName].deleteMessage(data); return await this.waMonitor.waInstances[instanceName].deleteMessage(data);

View File

@@ -3,25 +3,25 @@ import { isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { ConfigService, HttpServer, WaBusiness } from '../../config/env.config'; import { Auth, ConfigService, HttpServer, WaBusiness } from '../../config/env.config';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { BadRequestException, InternalServerErrorException } from '../../exceptions'; import { BadRequestException, InternalServerErrorException, UnauthorizedException } from '../../exceptions';
import { RedisCache } from '../../libs/redis.client';
import { InstanceDto, SetPresenceDto } from '../dto/instance.dto'; import { InstanceDto, SetPresenceDto } from '../dto/instance.dto';
import { ChatwootService } from '../integrations/chatwoot/services/chatwoot.service'; import { ChatwootService } from '../integrations/chatwoot/services/chatwoot.service';
import { RabbitmqService } from '../integrations/rabbitmq/services/rabbitmq.service'; import { RabbitmqService } from '../integrations/rabbitmq/services/rabbitmq.service';
import { SqsService } from '../integrations/sqs/services/sqs.service'; import { SqsService } from '../integrations/sqs/services/sqs.service';
import { TypebotService } from '../integrations/typebot/services/typebot.service'; import { TypebotService } from '../integrations/typebot/services/typebot.service';
import { WebsocketService } from '../integrations/websocket/services/websocket.service'; import { WebsocketService } from '../integrations/websocket/services/websocket.service';
import { ProviderFiles } from '../provider/sessions';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { AuthService, OldToken } from '../services/auth.service'; import { AuthService, OldToken } from '../services/auth.service';
import { CacheService } from '../services/cache.service'; import { CacheService } from '../services/cache.service';
import { BaileysStartupService } from '../services/channels/whatsapp.baileys.service';
import { BusinessStartupService } from '../services/channels/whatsapp.business.service';
import { IntegrationService } from '../services/integration.service'; import { IntegrationService } from '../services/integration.service';
import { WAMonitoringService } from '../services/monitor.service'; import { WAMonitoringService } from '../services/monitor.service';
import { SettingsService } from '../services/settings.service'; import { SettingsService } from '../services/settings.service';
import { WebhookService } from '../services/webhook.service'; import { WebhookService } from '../services/webhook.service';
import { BaileysStartupService } from '../services/whatsapp/whatsapp.baileys.service';
import { BusinessStartupService } from '../services/whatsapp/whatsapp.business.service';
import { Events, Integration, wa } from '../types/wa.types'; import { Events, Integration, wa } from '../types/wa.types';
import { ProxyController } from './proxy.controller'; import { ProxyController } from './proxy.controller';
@@ -41,8 +41,10 @@ export class InstanceController {
private readonly typebotService: TypebotService, private readonly typebotService: TypebotService,
private readonly integrationService: IntegrationService, private readonly integrationService: IntegrationService,
private readonly proxyService: ProxyController, private readonly proxyService: ProxyController,
private readonly cache: RedisCache, private readonly cache: CacheService,
private readonly chatwootCache: CacheService, private readonly chatwootCache: CacheService,
private readonly baileysCache: CacheService,
private readonly providerFiles: ProviderFiles,
) {} ) {}
private readonly logger = new Logger(InstanceController.name); private readonly logger = new Logger(InstanceController.name);
@@ -65,6 +67,8 @@ export class InstanceController {
chatwoot_reopen_conversation, chatwoot_reopen_conversation,
chatwoot_conversation_pending, chatwoot_conversation_pending,
chatwoot_import_contacts, chatwoot_import_contacts,
chatwoot_name_inbox,
chatwoot_merge_brazil_contacts,
chatwoot_import_messages, chatwoot_import_messages,
chatwoot_days_limit_import_messages, chatwoot_days_limit_import_messages,
reject_call, reject_call,
@@ -108,6 +112,8 @@ export class InstanceController {
this.repository, this.repository,
this.cache, this.cache,
this.chatwootCache, this.chatwootCache,
this.baileysCache,
this.providerFiles,
); );
} else { } else {
instance = new BaileysStartupService( instance = new BaileysStartupService(
@@ -116,6 +122,8 @@ export class InstanceController {
this.repository, this.repository,
this.cache, this.cache,
this.chatwootCache, this.chatwootCache,
this.baileysCache,
this.providerFiles,
); );
} }
@@ -511,11 +519,12 @@ export class InstanceController {
token: chatwoot_token, token: chatwoot_token,
url: chatwoot_url, url: chatwoot_url,
sign_msg: chatwoot_sign_msg || false, sign_msg: chatwoot_sign_msg || false,
name_inbox: instance.instanceName.split('-cwId-')[0], name_inbox: chatwoot_name_inbox ?? instance.instanceName.split('-cwId-')[0],
number, number,
reopen_conversation: chatwoot_reopen_conversation || false, reopen_conversation: chatwoot_reopen_conversation || false,
conversation_pending: chatwoot_conversation_pending || false, conversation_pending: chatwoot_conversation_pending || false,
import_contacts: chatwoot_import_contacts ?? true, import_contacts: chatwoot_import_contacts ?? true,
merge_brazil_contacts: chatwoot_merge_brazil_contacts ?? false,
import_messages: chatwoot_import_messages ?? true, import_messages: chatwoot_import_messages ?? true,
days_limit_import_messages: chatwoot_days_limit_import_messages ?? 60, days_limit_import_messages: chatwoot_days_limit_import_messages ?? 60,
auto_create: true, auto_create: true,
@@ -571,11 +580,12 @@ export class InstanceController {
sign_msg: chatwoot_sign_msg || false, sign_msg: chatwoot_sign_msg || false,
reopen_conversation: chatwoot_reopen_conversation || false, reopen_conversation: chatwoot_reopen_conversation || false,
conversation_pending: chatwoot_conversation_pending || false, conversation_pending: chatwoot_conversation_pending || false,
merge_brazil_contacts: chatwoot_merge_brazil_contacts ?? false,
import_contacts: chatwoot_import_contacts ?? true, import_contacts: chatwoot_import_contacts ?? true,
import_messages: chatwoot_import_messages ?? true, import_messages: chatwoot_import_messages ?? true,
days_limit_import_messages: chatwoot_days_limit_import_messages || 60, days_limit_import_messages: chatwoot_days_limit_import_messages || 60,
number, number,
name_inbox: instance.instanceName, name_inbox: chatwoot_name_inbox ?? instance.instanceName,
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
}, },
}; };
@@ -673,11 +683,26 @@ export class InstanceController {
}; };
} }
public async fetchInstances({ instanceName, instanceId, number }: InstanceDto) { public async fetchInstances({ instanceName, instanceId, number }: InstanceDto, key: string) {
if (instanceName) { const env = this.configService.get<Auth>('AUTHENTICATION').API_KEY;
this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance');
this.logger.verbose('instanceName: ' + instanceName); let name = instanceName;
return this.waMonitor.instanceInfo(instanceName); let arrayReturn = false;
if (env.KEY !== key) {
const instanceByKey = await this.repository.auth.findByKey(key);
if (instanceByKey) {
name = instanceByKey._id;
arrayReturn = true;
} else {
throw new UnauthorizedException();
}
}
if (name) {
this.logger.verbose('requested fetchInstances from ' + name + ' instance');
this.logger.verbose('instanceName: ' + name);
return this.waMonitor.instanceInfo(name, arrayReturn);
} else if (instanceId || number) { } else if (instanceId || number) {
return this.waMonitor.instanceInfoById(instanceId, number); return this.waMonitor.instanceInfoById(instanceId, number);
} }
@@ -728,7 +753,7 @@ export class InstanceController {
this.logger.verbose('deleting instance: ' + instanceName); this.logger.verbose('deleting instance: ' + instanceName);
try { try {
this.waMonitor.waInstances[instanceName].sendDataWebhook(Events.INSTANCE_DELETE, { this.waMonitor.waInstances[instanceName]?.sendDataWebhook(Events.INSTANCE_DELETE, {
instanceName, instanceName,
instanceId: (await this.repository.auth.find(instanceName))?.instanceId, instanceId: (await this.repository.auth.find(instanceName))?.instanceId,
}); });

View File

@@ -73,6 +73,11 @@ export class ArchiveChatDto {
archive: boolean; archive: boolean;
} }
export class MarkChatUnreadDto {
lastMessage?: LastMessage;
chat?: string;
}
class PrivacySetting { class PrivacySetting {
readreceipts: WAReadReceiptsValue; readreceipts: WAReadReceiptsValue;
profile: WAPrivacyValue; profile: WAPrivacyValue;

View File

@@ -27,9 +27,11 @@ export class InstanceDto {
chatwoot_sign_msg?: boolean; chatwoot_sign_msg?: boolean;
chatwoot_reopen_conversation?: boolean; chatwoot_reopen_conversation?: boolean;
chatwoot_conversation_pending?: boolean; chatwoot_conversation_pending?: boolean;
chatwoot_merge_brazil_contacts?: boolean;
chatwoot_import_contacts?: boolean; chatwoot_import_contacts?: boolean;
chatwoot_import_messages?: boolean; chatwoot_import_messages?: boolean;
chatwoot_days_limit_import_messages?: number; chatwoot_days_limit_import_messages?: number;
chatwoot_name_inbox?: string;
websocket_enabled?: boolean; websocket_enabled?: boolean;
websocket_events?: string[]; websocket_events?: string[];
rabbitmq_enabled?: boolean; rabbitmq_enabled?: boolean;

View File

@@ -3,7 +3,7 @@ import { NextFunction, Request, Response } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { name } from '../../../package.json'; import { name } from '../../../package.json';
import { Auth, configService } from '../../config/env.config'; import { Auth, configService, Database } from '../../config/env.config';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { ForbiddenException, UnauthorizedException } from '../../exceptions'; import { ForbiddenException, UnauthorizedException } from '../../exceptions';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
@@ -58,6 +58,11 @@ async function jwtGuard(req: Request, res: Response, next: NextFunction) {
async function apikey(req: Request, _: Response, next: NextFunction) { async function apikey(req: Request, _: Response, next: NextFunction) {
const env = configService.get<Auth>('AUTHENTICATION').API_KEY; const env = configService.get<Auth>('AUTHENTICATION').API_KEY;
const key = req.get('apikey'); const key = req.get('apikey');
const db = configService.get<Database>('DATABASE');
if (!key) {
throw new UnauthorizedException();
}
if (env.KEY === key) { if (env.KEY === key) {
return next(); return next();
@@ -66,12 +71,21 @@ async function apikey(req: Request, _: Response, next: NextFunction) {
if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) { if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) {
throw new ForbiddenException('Missing global api key', 'The global api key must be set'); throw new ForbiddenException('Missing global api key', 'The global api key must be set');
} }
const param = req.params as unknown as InstanceDto;
try { try {
const param = req.params as unknown as InstanceDto; if (param?.instanceName) {
const instanceKey = await repository.auth.find(param.instanceName); const instanceKey = await repository.auth.find(param.instanceName);
if (instanceKey.apikey === key) { if (instanceKey?.apikey === key) {
return next(); return next();
}
} else {
if (req.originalUrl.includes('/instance/fetchInstances') && db.ENABLED) {
const instanceByKey = await repository.auth.findByKey(key);
if (instanceByKey) {
return next();
}
}
} }
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);

View File

@@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from 'express';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { configService, Database, Redis } from '../../config/env.config'; import { CacheConf, configService, Database } from '../../config/env.config';
import { INSTANCE_DIR } from '../../config/path.config'; import { INSTANCE_DIR } from '../../config/path.config';
import { import {
BadRequestException, BadRequestException,
@@ -17,12 +17,13 @@ import { cache, waMonitor } from '../server.module';
async function getInstance(instanceName: string) { async function getInstance(instanceName: string) {
try { try {
const db = configService.get<Database>('DATABASE'); const db = configService.get<Database>('DATABASE');
const redisConf = configService.get<Redis>('REDIS'); const cacheConf = configService.get<CacheConf>('CACHE');
const exists = !!waMonitor.waInstances[instanceName]; const exists = !!waMonitor.waInstances[instanceName];
if (redisConf.ENABLED) { if (cacheConf.REDIS.ENABLED && cacheConf.REDIS.SAVE_INSTANCES) {
const keyExists = await cache.keyExists(); const keyExists = await cache.has(instanceName);
return exists || keyExists; return exists || keyExists;
} }

View File

@@ -1,5 +1,6 @@
import { isURL } from 'class-validator'; import { isURL } from 'class-validator';
import { CacheEngine } from '../../../../cache/cacheengine';
import { ConfigService, HttpServer } from '../../../../config/env.config'; import { ConfigService, HttpServer } from '../../../../config/env.config';
import { Logger } from '../../../../config/logger.config'; import { Logger } from '../../../../config/logger.config';
import { BadRequestException } from '../../../../exceptions'; import { BadRequestException } from '../../../../exceptions';
@@ -7,7 +8,6 @@ import { InstanceDto } from '../../../dto/instance.dto';
import { RepositoryBroker } from '../../../repository/repository.manager'; import { RepositoryBroker } from '../../../repository/repository.manager';
import { waMonitor } from '../../../server.module'; import { waMonitor } from '../../../server.module';
import { CacheService } from '../../../services/cache.service'; import { CacheService } from '../../../services/cache.service';
import { CacheEngine } from '../cache/cacheengine';
import { ChatwootDto } from '../dto/chatwoot.dto'; import { ChatwootDto } from '../dto/chatwoot.dto';
import { ChatwootService } from '../services/chatwoot.service'; import { ChatwootService } from '../services/chatwoot.service';
@@ -53,11 +53,15 @@ export class ChatwootController {
data.conversation_pending = false; data.conversation_pending = false;
data.import_contacts = false; data.import_contacts = false;
data.import_messages = false; data.import_messages = false;
data.merge_brazil_contacts = false;
data.days_limit_import_messages = 0; data.days_limit_import_messages = 0;
data.auto_create = false; data.auto_create = false;
data.name_inbox = '';
} }
data.name_inbox = instance.instanceName; if (!data.name_inbox || data.name_inbox === '') {
data.name_inbox = instance.instanceName;
}
const result = await this.chatwootService.create(instance, data); const result = await this.chatwootService.create(instance, data);

View File

@@ -9,6 +9,7 @@ export class ChatwootDto {
number?: string; number?: string;
reopen_conversation?: boolean; reopen_conversation?: boolean;
conversation_pending?: boolean; conversation_pending?: boolean;
merge_brazil_contacts?: boolean;
import_contacts?: boolean; import_contacts?: boolean;
import_messages?: boolean; import_messages?: boolean;
days_limit_import_messages?: number; days_limit_import_messages?: number;

View File

@@ -14,6 +14,7 @@ export class ChatwootRaw {
number?: string; number?: string;
reopen_conversation?: boolean; reopen_conversation?: boolean;
conversation_pending?: boolean; conversation_pending?: boolean;
merge_brazil_contacts?: boolean;
import_contacts?: boolean; import_contacts?: boolean;
import_messages?: boolean; import_messages?: boolean;
days_limit_import_messages?: number; days_limit_import_messages?: number;
@@ -31,6 +32,7 @@ const chatwootSchema = new Schema<ChatwootRaw>({
number: { type: String, required: true }, number: { type: String, required: true },
reopen_conversation: { type: Boolean, required: true }, reopen_conversation: { type: Boolean, required: true },
conversation_pending: { type: Boolean, required: true }, conversation_pending: { type: Boolean, required: true },
merge_brazil_contacts: { type: Boolean, required: true },
import_contacts: { type: Boolean, required: true }, import_contacts: { type: Boolean, required: true },
import_messages: { type: Boolean, required: true }, import_messages: { type: Boolean, required: true },
days_limit_import_messages: { type: Number, required: true }, days_limit_import_messages: { type: Number, required: true },

View File

@@ -8,6 +8,7 @@ import ChatwootClient, {
inbox, inbox,
} from '@figuro/chatwoot-sdk'; } from '@figuro/chatwoot-sdk';
import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request'; import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request';
import { proto } from '@whiskeysockets/baileys';
import axios from 'axios'; import axios from 'axios';
import FormData from 'form-data'; import FormData from 'form-data';
import { createReadStream, unlinkSync, writeFileSync } from 'fs'; import { createReadStream, unlinkSync, writeFileSync } from 'fs';
@@ -85,12 +86,14 @@ export class ChatwootService {
return client; return client;
} }
public getClientCwConfig(): ChatwootAPIConfig { public getClientCwConfig(): ChatwootAPIConfig & { name_inbox: string; merge_brazil_contacts: boolean } {
return { return {
basePath: this.provider.url, basePath: this.provider.url,
with_credentials: true, with_credentials: true,
credentials: 'include', credentials: 'include',
token: this.provider.token, token: this.provider.token,
name_inbox: this.provider.name_inbox,
merge_brazil_contacts: this.provider.merge_brazil_contacts,
}; };
} }
@@ -110,7 +113,7 @@ export class ChatwootService {
await this.initInstanceChatwoot( await this.initInstanceChatwoot(
instance, instance,
instance.instanceName.split('-cwId-')[0], data.name_inbox ?? instance.instanceName.split('-cwId-')[0],
`${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
true, true,
data.number, data.number,
@@ -416,10 +419,49 @@ export class ChatwootService {
} }
} }
private async mergeBrazilianContacts(contacts: any[]) {
try {
//sdk chatwoot não tem função merge
this.logger.verbose('merging contacts');
const contact = await chatwootRequest(this.getClientCwConfig(), {
method: 'POST',
url: `/api/v1/accounts/${this.provider.account_id}/actions/contact_merge`,
body: {
base_contact_id: contacts.find((contact) => contact.phone_number.length === 14)?.id,
mergee_contact_id: contacts.find((contact) => contact.phone_number.length === 13)?.id,
},
});
return contact;
} catch {
this.logger.error('Error merging contacts');
return null;
}
}
private findContactInContactList(contacts: any[], query: string) { private findContactInContactList(contacts: any[], query: string) {
const phoneNumbers = this.getNumbers(query); const phoneNumbers = this.getNumbers(query);
const searchableFields = this.getSearchableFields(); const searchableFields = this.getSearchableFields();
// eslint-disable-next-line prettier/prettier
if(contacts.length === 2 && this.getClientCwConfig().merge_brazil_contacts && query.startsWith('+55')){
const contact = this.mergeBrazilianContacts(contacts);
if (contact) {
return contact;
}
}
const phone = phoneNumbers.reduce(
(savedNumber, number) => (number.length > savedNumber.length ? number : savedNumber),
'',
);
const contact_with9 = contacts.find((contact) => contact.phone_number === phone);
if (contact_with9) {
return contact_with9;
}
for (const contact of contacts) { for (const contact of contacts) {
for (const field of searchableFields) { for (const field of searchableFields) {
if (contact[field] && phoneNumbers.includes(contact[field])) { if (contact[field] && phoneNumbers.includes(contact[field])) {
@@ -447,7 +489,7 @@ export class ChatwootService {
} }
private getSearchableFields() { private getSearchableFields() {
return ['phone_number', 'identifier']; return ['phone_number'];
} }
private getFilterPayload(query: string) { private getFilterPayload(query: string) {
@@ -461,7 +503,7 @@ export class ChatwootService {
const queryOperator = fieldsToSearch.length - 1 === index1 && numbers.length - 1 === index2 ? null : 'OR'; const queryOperator = fieldsToSearch.length - 1 === index1 && numbers.length - 1 === index2 ? null : 'OR';
filterPayload.push({ filterPayload.push({
attribute_key: field, attribute_key: field,
filter_operator: field == 'phone_number' ? 'equal_to' : 'contains', filter_operator: 'equal_to',
values: [number.replace('+', '')], values: [number.replace('+', '')],
query_operator: queryOperator, query_operator: queryOperator,
}); });
@@ -559,53 +601,44 @@ export class ChatwootService {
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
const findContact = await this.findContact(instance, chatId); let contact = await this.findContact(instance, chatId);
let contact: any; if (contact) {
if (body.key.fromMe) { if (!body.key.fromMe) {
if (findContact) { const waProfilePictureFile =
contact = await this.updateContact(instance, findContact.id, { picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
avatar_url: picture_url.profilePictureUrl || null, const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
}); const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
} else { const nameNeedsUpdate =
const jid = isGroup ? null : body.key.remoteJid; !contact.name ||
contact = await this.createContact( contact.name === chatId ||
instance, (`+${chatId}`.startsWith('+55')
chatId, ? this.getNumbers(`+${chatId}`).some(
filterInbox.id, (v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
isGroup, )
nameContact, : false);
picture_url.profilePictureUrl || null,
jid, const contactNeedsUpdate = pictureNeedsUpdate || nameNeedsUpdate;
); if (contactNeedsUpdate) {
this.logger.verbose('update contact in chatwoot');
contact = await this.updateContact(instance, contact.id, {
...(nameNeedsUpdate && { name: nameContact }),
...(waProfilePictureFile === '' && { avatar: null }),
...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
});
}
} }
} else { } else {
if (findContact) { const jid = isGroup ? null : body.key.remoteJid;
if (!findContact.name || findContact.name === chatId) { contact = await this.createContact(
contact = await this.updateContact(instance, findContact.id, { instance,
name: nameContact, chatId,
avatar_url: picture_url.profilePictureUrl || null, filterInbox.id,
}); isGroup,
} else { nameContact,
contact = await this.updateContact(instance, findContact.id, { picture_url.profilePictureUrl || null,
avatar_url: picture_url.profilePictureUrl || null, jid,
}); );
}
if (!contact) {
contact = await this.findContact(instance, chatId);
}
} else {
const jid = isGroup ? null : body.key.remoteJid;
contact = await this.createContact(
instance,
chatId,
filterInbox.id,
isGroup,
nameContact,
picture_url.profilePictureUrl || null,
jid,
);
}
} }
if (!contact) { if (!contact) {
@@ -615,20 +648,13 @@ export class ChatwootService {
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) {
this.logger.verbose('update contact name in chatwoot');
await this.updateContact(instance, contactId, {
name: nameContact,
});
}
this.logger.verbose('get contact conversations in chatwoot'); this.logger.verbose('get contact conversations in chatwoot');
const contactConversations = (await client.contacts.listConversations({ const contactConversations = (await client.contacts.listConversations({
accountId: this.provider.account_id, accountId: this.provider.account_id,
id: contactId, id: contactId,
})) as any; })) as any;
if (contactConversations) { if (contactConversations?.payload?.length) {
let conversation: any; let conversation: any;
if (this.provider.reopen_conversation) { if (this.provider.reopen_conversation) {
conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id); conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id);
@@ -710,7 +736,7 @@ export class ChatwootService {
} }
this.logger.verbose('find inbox by name'); this.logger.verbose('find inbox by name');
const findByName = inbox.payload.find((inbox) => inbox.name === instance.instanceName.split('-cwId-')[0]); const findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().name_inbox);
if (!findByName) { if (!findByName) {
this.logger.warn('inbox not found'); this.logger.warn('inbox not found');
@@ -787,26 +813,15 @@ export class ChatwootService {
return null; return null;
} }
const payload = [ const conversations = (await client.contacts.listConversations({
['inbox_id', inbox.id.toString()], accountId: this.provider.account_id,
['contact_id', contact.id.toString()], id: contact.id,
['status', 'open'], })) as any;
];
return ( return (
( conversations.payload.find(
(await client.conversations.filter({ (conversation) => conversation.inbox_id === inbox.id && conversation.status === 'open',
accountId: this.provider.account_id, ) || undefined
payload: payload.map((item, i, payload) => {
return {
attribute_key: item[0],
filter_operator: 'equal_to',
values: [item[1]],
query_operator: i < payload.length - 1 ? 'AND' : null,
};
}),
})) as { payload: conversation[] }
).payload[0] || undefined
); );
} }
@@ -1077,6 +1092,10 @@ export class ChatwootService {
return messageSent; return messageSent;
} }
if (type === 'image' && parsedMedia && parsedMedia?.ext === '.gif') {
type = 'document';
}
this.logger.verbose('send media to instance: ' + waInstance.instanceName); this.logger.verbose('send media to instance: ' + waInstance.instanceName);
const data: SendMediaDto = { const data: SendMediaDto = {
number: number, number: number,
@@ -1106,6 +1125,26 @@ export class ChatwootService {
} }
} }
public async onSendMessageError(instance: InstanceDto, conversation: number, error?: string) {
const client = await this.clientCw(instance);
if (!client) {
return;
}
client.messages.create({
accountId: this.provider.account_id,
conversationId: conversation,
data: {
content: i18next.t('cw.message.notsent', {
error: error?.length > 0 ? `_${error}_` : '',
}),
message_type: 'outgoing',
private: true,
},
});
}
public async receiveWebhook(instance: InstanceDto, body: any) { public async receiveWebhook(instance: InstanceDto, body: any) {
try { try {
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
@@ -1274,6 +1313,11 @@ export class ChatwootService {
return { message: 'bot' }; return { message: 'bot' };
} }
if (!waInstance && body.conversation?.id) {
this.onSendMessageError(instance, body.conversation?.id, 'Instance not found');
return { message: 'bot' };
}
this.logger.verbose('Format message to send'); this.logger.verbose('Format message to send');
let formatText: string; let formatText: string;
if (senderName === null || senderName === undefined) { if (senderName === null || senderName === undefined) {
@@ -1310,6 +1354,9 @@ export class ChatwootService {
formatText, formatText,
options, options,
); );
if (!messageSent && body.conversation?.id) {
this.onSendMessageError(instance, body.conversation?.id);
}
this.updateChatwootMessageId( this.updateChatwootMessageId(
{ {
@@ -1343,23 +1390,34 @@ export class ChatwootService {
}, },
}; };
const messageSent = await waInstance?.textMessage(data, true); let messageSent: MessageRaw | proto.WebMessageInfo;
try {
messageSent = await waInstance?.textMessage(data, true);
if (!messageSent) {
throw new Error('Message not sent');
}
this.updateChatwootMessageId( this.updateChatwootMessageId(
{ {
...messageSent, ...messageSent,
owner: instance.instanceName, owner: instance.instanceName,
},
{
messageId: body.id,
inboxId: body.inbox?.id,
conversationId: body.conversation?.id,
contactInbox: {
sourceId: body.conversation?.contact_inbox?.source_id,
}, },
}, {
instance, messageId: body.id,
); inboxId: body.inbox?.id,
conversationId: body.conversation?.id,
contactInbox: {
sourceId: body.conversation?.contact_inbox?.source_id,
},
},
instance,
);
} catch (error) {
if (!messageSent && body.conversation?.id) {
this.onSendMessageError(instance, body.conversation?.id, error.toString());
}
throw error;
}
} }
} }

View File

@@ -30,10 +30,12 @@ export const chatwootSchema: JSONSchema7 = {
url: { type: 'string' }, url: { type: 'string' },
sign_msg: { type: 'boolean', enum: [true, false] }, sign_msg: { type: 'boolean', enum: [true, false] },
sign_delimiter: { type: ['string', 'null'] }, sign_delimiter: { type: ['string', 'null'] },
name_inbox: { type: ['string', 'null'] },
reopen_conversation: { type: 'boolean', enum: [true, false] }, reopen_conversation: { type: 'boolean', enum: [true, false] },
conversation_pending: { type: 'boolean', enum: [true, false] }, conversation_pending: { type: 'boolean', enum: [true, false] },
auto_create: { type: 'boolean', enum: [true, false] }, auto_create: { type: 'boolean', enum: [true, false] },
import_contacts: { type: 'boolean', enum: [true, false] }, import_contacts: { type: 'boolean', enum: [true, false] },
merge_brazil_contacts: { type: 'boolean', enum: [true, false] },
import_messages: { type: 'boolean', enum: [true, false] }, import_messages: { type: 'boolean', enum: [true, false] },
days_limit_import_messages: { type: 'number' }, days_limit_import_messages: { type: 'number' },
}, },

View File

@@ -42,6 +42,41 @@ export const getAMQP = (): amqp.Channel | null => {
return amqpChannel; return amqpChannel;
}; };
export const initGlobalQueues = () => {
logger.info('Initializing global queues');
const events = configService.get<Rabbitmq>('RABBITMQ').EVENTS;
if (!events) {
logger.warn('No events to initialize on AMQP');
return;
}
const eventKeys = Object.keys(events);
eventKeys.forEach((event) => {
if (events[event] === false) return;
const queueName = `${event.replace(/_/g, '.').toLowerCase()}`;
const amqp = getAMQP();
const exchangeName = 'evolution_exchange';
amqp.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
amqp.assertQueue(queueName, {
durable: true,
autoDelete: false,
arguments: {
'x-queue-type': 'quorum',
},
});
amqp.bindQueue(queueName, exchangeName, event);
});
};
export const initQueues = (instanceName: string, events: string[]) => { export const initQueues = (instanceName: string, events: string[]) => {
if (!events || !events.length) return; if (!events || !events.length) return;

View File

@@ -1,4 +1,4 @@
import { SQS } from 'aws-sdk'; import { SQS } from '@aws-sdk/client-sqs';
import { configService, Sqs } from '../../../../config/env.config'; import { configService, Sqs } from '../../../../config/env.config';
import { Logger } from '../../../../config/logger.config'; import { Logger } from '../../../../config/logger.config';
@@ -12,8 +12,11 @@ export const initSQS = () => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const awsConfig = configService.get<Sqs>('SQS'); const awsConfig = configService.get<Sqs>('SQS');
sqs = new SQS({ sqs = new SQS({
accessKeyId: awsConfig.ACCESS_KEY_ID, credentials: {
secretAccessKey: awsConfig.SECRET_ACCESS_KEY, accessKeyId: awsConfig.ACCESS_KEY_ID,
secretAccessKey: awsConfig.SECRET_ACCESS_KEY,
},
region: awsConfig.REGION, region: awsConfig.REGION,
}); });

View File

@@ -57,7 +57,7 @@ export class TypebotService {
if (session) { if (session) {
if (status === 'closed') { if (status === 'closed') {
const found_session: Session[] = findData.sessions.splice(findData.sessions.indexOf(session), 1); findData.sessions.splice(findData.sessions.indexOf(session), 1);
const typebotData = { const typebotData = {
enabled: findData.enabled, enabled: findData.enabled,
@@ -68,7 +68,7 @@ export class TypebotService {
delay_message: findData.delay_message, delay_message: findData.delay_message,
unknown_message: findData.unknown_message, unknown_message: findData.unknown_message,
listening_from_me: findData.listening_from_me, listening_from_me: findData.listening_from_me,
sessions: found_session, sessions: findData.sessions,
}; };
this.create(instance, typebotData); this.create(instance, typebotData);
@@ -106,7 +106,7 @@ export class TypebotService {
delay_message: findData.delay_message, delay_message: findData.delay_message,
unknown_message: findData.unknown_message, unknown_message: findData.unknown_message,
listening_from_me: findData.listening_from_me, listening_from_me: findData.listening_from_me,
sessions: findData.sessions.splice(findData.sessions.indexOf(session), 1), sessions: findData.sessions,
}; };
this.create(instance, typebotData); this.create(instance, typebotData);
@@ -519,18 +519,36 @@ export class TypebotService {
text += element.text; text += element.text;
} }
if ( if (element.children && element.type !== 'a') {
element.children &&
(element.type === 'p' ||
element.type === 'a' ||
element.type === 'inline-variable' ||
element.type === 'variable')
) {
for (const child of element.children) { for (const child of element.children) {
text += applyFormatting(child); text += applyFormatting(child);
} }
} }
if (element.type === 'p' && element.type !== 'inline-variable') {
text = text.trim() + '\n';
}
if (element.type === 'inline-variable') {
text = text.trim();
}
if (element.type === 'ol') {
text =
'\n' +
text
.split('\n')
.map((line, index) => (line ? `${index + 1}. ${line}` : ''))
.join('\n');
}
if (element.type === 'li') {
text = text
.split('\n')
.map((line) => (line ? ` ${line}` : ''))
.join('\n');
}
let formats = ''; let formats = '';
if (element.bold) { if (element.bold) {
@@ -568,6 +586,8 @@ export class TypebotService {
formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, ''); formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, '');
formattedText = formattedText.replace(/\n$/, '');
await instance.textMessage({ await instance.textMessage({
number: remoteJid.split('@')[0], number: remoteJid.split('@')[0],
options: { options: {

View File

@@ -9,12 +9,12 @@ export class IntegrationRaw {
token?: string; token?: string;
} }
const sqsSchema = new Schema<IntegrationRaw>({ const integrationSchema = new Schema<IntegrationRaw>({
_id: { type: String, _id: true }, _id: { type: String, _id: true },
integration: { type: String, required: true }, integration: { type: String, required: true },
number: { type: String, required: true }, number: { type: String, required: true },
token: { type: String, required: true }, token: { type: String, required: true },
}); });
export const IntegrationModel = dbserver?.model(IntegrationRaw.name, sqsSchema, 'integration'); export const IntegrationModel = dbserver?.model(IntegrationRaw.name, integrationSchema, 'integration');
export type IntegrationModel = typeof IntegrationModel; export type IntegrationModel = typeof IntegrationModel;

View File

@@ -32,6 +32,7 @@ export class MessageRaw {
source_reply_id?: string; source_reply_id?: string;
chatwoot?: ChatwootMessage; chatwoot?: ChatwootMessage;
contextInfo?: any; contextInfo?: any;
status?: wa.StatusMessage | any;
} }
type MessageRawBoolean<T> = { type MessageRawBoolean<T> = {

View File

@@ -0,0 +1,150 @@
import axios from 'axios';
import { execSync } from 'child_process';
import { Auth, ConfigService, ProviderSession } from '../../config/env.config';
import { Logger } from '../../config/logger.config';
type ResponseSuccess = { status: number; data?: any };
type ResponseProvider = Promise<[ResponseSuccess?, Error?]>;
export class ProviderFiles {
constructor(private readonly configService: ConfigService) {
this.baseUrl = `http://${this.config.HOST}:${this.config.PORT}/session/${this.config.PREFIX}`;
this.globalApiToken = this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;
}
private readonly logger = new Logger(ProviderFiles.name);
private baseUrl: string;
private globalApiToken: string;
private readonly config = Object.freeze(this.configService.get<ProviderSession>('PROVIDER'));
get isEnabled() {
return !!this.config?.ENABLED;
}
public async onModuleInit() {
if (this.config.ENABLED) {
const url = `http://${this.config.HOST}:${this.config.PORT}`;
try {
const response = await axios.options(url + '/ping');
if (response?.data != 'pong') {
throw new Error('Offline file provider.');
}
await axios.post(`${url}/session`, { group: this.config.PREFIX }, { headers: { apikey: this.globalApiToken } });
} catch (error) {
this.logger.error(['Failed to connect to the file server', error?.message, error?.stack]);
const pid = process.pid;
execSync(`kill -9 ${pid}`);
}
}
}
public async onModuleDestroy() {
//
}
public async create(instance: string): ResponseProvider {
try {
const response = await axios.post(
`${this.baseUrl}`,
{
instance,
},
{ headers: { apikey: this.globalApiToken } },
);
return [{ status: response.status, data: response?.data }];
} catch (error) {
return [
{
status: error?.response?.status,
data: error?.response?.data,
},
error,
];
}
}
public async write(instance: string, key: string, data: any): ResponseProvider {
try {
const response = await axios.post(`${this.baseUrl}/${instance}/${key}`, data, {
headers: { apikey: this.globalApiToken },
});
return [{ status: response.status, data: response?.data }];
} catch (error) {
return [
{
status: error?.response?.status,
data: error?.response?.data,
},
error,
];
}
}
public async read(instance: string, key: string): ResponseProvider {
try {
const response = await axios.get(`${this.baseUrl}/${instance}/${key}`, {
headers: { apikey: this.globalApiToken },
});
return [{ status: response.status, data: response?.data }];
} catch (error) {
return [
{
status: error?.response?.status,
data: error?.response?.data,
},
error,
];
}
}
public async delete(instance: string, key: string): ResponseProvider {
try {
const response = await axios.delete(`${this.baseUrl}/${instance}/${key}`, {
headers: { apikey: this.globalApiToken },
});
return [{ status: response.status, data: response?.data }];
} catch (error) {
return [
{
status: error?.response?.status,
data: error?.response?.data,
},
error,
];
}
}
public async allInstances(): ResponseProvider {
try {
const response = await axios.get(`${this.baseUrl}/list-instances`, { headers: { apikey: this.globalApiToken } });
return [{ status: response.status, data: response?.data as string[] }];
} catch (error) {
return [
{
status: error?.response?.status,
data: error?.response?.data,
},
error,
];
}
}
public async removeSession(instance: string): ResponseProvider {
try {
const response = await axios.delete(`${this.baseUrl}/${instance}`, { headers: { apikey: this.globalApiToken } });
return [{ status: response.status, data: response?.data }];
} catch (error) {
return [
{
status: error?.response?.status,
data: error?.response?.data,
},
error,
];
}
}
}

View File

@@ -68,6 +68,20 @@ export class AuthRepository extends Repository {
} }
} }
public async findByKey(key: string): Promise<AuthRaw> {
try {
this.logger.verbose('finding auth');
if (this.dbSettings.ENABLED) {
this.logger.verbose('finding auth in db');
return await this.authModel.findOne({ apikey: key });
}
return {};
} catch (error) {
return {};
}
}
public async list(): Promise<AuthRaw[]> { public async list(): Promise<AuthRaw[]> {
try { try {
if (this.dbSettings.ENABLED) { if (this.dbSettings.ENABLED) {

View File

@@ -6,6 +6,7 @@ import {
blockUserSchema, blockUserSchema,
contactValidateSchema, contactValidateSchema,
deleteMessageSchema, deleteMessageSchema,
markChatUnreadSchema,
messageUpSchema, messageUpSchema,
messageValidateSchema, messageValidateSchema,
presenceSchema, presenceSchema,
@@ -24,6 +25,7 @@ import {
BlockUserDto, BlockUserDto,
DeleteMessage, DeleteMessage,
getBase64FromMediaMessageDto, getBase64FromMediaMessageDto,
MarkChatUnreadDto,
NumberDto, NumberDto,
PrivacySettingDto, PrivacySettingDto,
ProfileNameDto, ProfileNameDto,
@@ -98,6 +100,23 @@ export class ChatRouter extends RouterBroker {
return res.status(HttpStatus.CREATED).json(response); return res.status(HttpStatus.CREATED).json(response);
}) })
.put(this.routerPath('markChatUnread'), ...guards, async (req, res) => {
logger.verbose('request received in markChatUnread');
logger.verbose('request body: ');
logger.verbose(req.body);
logger.verbose('request query: ');
logger.verbose(req.query);
const response = await this.dataValidate<MarkChatUnreadDto>({
request: req,
schema: markChatUnreadSchema,
ClassRef: MarkChatUnreadDto,
execute: (instance, data) => chatController.markChatUnread(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.delete(this.routerPath('deleteMessageForEveryone'), ...guards, async (req, res) => { .delete(this.routerPath('deleteMessageForEveryone'), ...guards, async (req, res) => {
logger.verbose('request received in deleteMessageForEveryone'); logger.verbose('request received in deleteMessageForEveryone');
logger.verbose('request body: '); logger.verbose('request body: ');

View File

@@ -103,13 +103,15 @@ export class InstanceRouter extends RouterBroker {
logger.verbose('request body: '); logger.verbose('request body: ');
logger.verbose(req.body); logger.verbose(req.body);
const key = req.get('apikey');
logger.verbose('request query: '); logger.verbose('request query: ');
logger.verbose(req.query); logger.verbose(req.query);
const response = await this.dataValidate<InstanceDto>({ const response = await this.dataValidate<InstanceDto>({
request: req, request: req,
schema: null, schema: null,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => instanceController.fetchInstances(instance), execute: (instance) => instanceController.fetchInstances(instance, key),
}); });
return res.status(HttpStatus.OK).json(response); return res.status(HttpStatus.OK).json(response);

View File

@@ -1,8 +1,8 @@
import { configService } from '../config/env.config'; import { CacheEngine } from '../cache/cacheengine';
import { configService, ProviderSession } from '../config/env.config';
import { eventEmitter } from '../config/event.config'; import { eventEmitter } from '../config/event.config';
import { Logger } from '../config/logger.config'; import { Logger } from '../config/logger.config';
import { dbserver } from '../libs/db.connect'; import { dbserver } from '../libs/db.connect';
import { RedisCache } from '../libs/redis.client';
import { ChatController } from './controllers/chat.controller'; import { ChatController } from './controllers/chat.controller';
import { GroupController } from './controllers/group.controller'; import { GroupController } from './controllers/group.controller';
import { InstanceController } from './controllers/instance.controller'; import { InstanceController } from './controllers/instance.controller';
@@ -14,7 +14,6 @@ import { WebhookController } from './controllers/webhook.controller';
import { ChamaaiController } from './integrations/chamaai/controllers/chamaai.controller'; import { ChamaaiController } from './integrations/chamaai/controllers/chamaai.controller';
import { ChamaaiRepository } from './integrations/chamaai/repository/chamaai.repository'; import { ChamaaiRepository } from './integrations/chamaai/repository/chamaai.repository';
import { ChamaaiService } from './integrations/chamaai/services/chamaai.service'; import { ChamaaiService } from './integrations/chamaai/services/chamaai.service';
import { CacheEngine } from './integrations/chatwoot/cache/cacheengine';
import { ChatwootController } from './integrations/chatwoot/controllers/chatwoot.controller'; import { ChatwootController } from './integrations/chatwoot/controllers/chatwoot.controller';
import { ChatwootRepository } from './integrations/chatwoot/repository/chatwoot.repository'; import { ChatwootRepository } from './integrations/chatwoot/repository/chatwoot.repository';
import { ChatwootService } from './integrations/chatwoot/services/chatwoot.service'; import { ChatwootService } from './integrations/chatwoot/services/chatwoot.service';
@@ -48,6 +47,7 @@ import {
WebsocketModel, WebsocketModel,
} from './models'; } from './models';
import { LabelModel } from './models/label.model'; import { LabelModel } from './models/label.model';
import { ProviderFiles } from './provider/sessions';
import { AuthRepository } from './repository/auth.repository'; import { AuthRepository } from './repository/auth.repository';
import { ChatRepository } from './repository/chat.repository'; import { ChatRepository } from './repository/chat.repository';
import { ContactRepository } from './repository/contact.repository'; import { ContactRepository } from './repository/contact.repository';
@@ -107,10 +107,25 @@ export const repository = new RepositoryBroker(
dbserver?.getClient(), dbserver?.getClient(),
); );
export const cache = new RedisCache(); export const cache = new CacheService(new CacheEngine(configService, 'instance').getEngine());
const chatwootCache = new CacheService(new CacheEngine(configService, ChatwootService.name).getEngine()); const chatwootCache = new CacheService(new CacheEngine(configService, ChatwootService.name).getEngine());
const baileysCache = new CacheService(new CacheEngine(configService, 'baileys').getEngine());
export const waMonitor = new WAMonitoringService(eventEmitter, configService, repository, cache, chatwootCache); let providerFiles: ProviderFiles = null;
if (configService.get<ProviderSession>('PROVIDER')?.ENABLED) {
providerFiles = new ProviderFiles(configService);
}
export const waMonitor = new WAMonitoringService(
eventEmitter,
configService,
repository,
cache,
chatwootCache,
baileysCache,
providerFiles,
);
const authService = new AuthService(configService, waMonitor, repository); const authService = new AuthService(configService, waMonitor, repository);
@@ -160,6 +175,8 @@ export const instanceController = new InstanceController(
proxyController, proxyController,
cache, cache,
chatwootCache, chatwootCache,
baileysCache,
providerFiles,
); );
export const sendMessageController = new SendMessageController(waMonitor); export const sendMessageController = new SendMessageController(waMonitor);
export const chatController = new ChatController(waMonitor); export const chatController = new ChatController(waMonitor);

View File

@@ -1,3 +1,5 @@
import { BufferJSON } from '@whiskeysockets/baileys';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { ICache } from '../abstract/abstract.cache'; import { ICache } from '../abstract/abstract.cache';
@@ -20,6 +22,21 @@ export class CacheService {
return this.cache.get(key); return this.cache.get(key);
} }
public async hGet(key: string, field: string) {
try {
const data = await this.cache.hGet(key, field);
if (data) {
return JSON.parse(data, BufferJSON.reviver);
}
return null;
} catch (error) {
this.logger.error(error);
return null;
}
}
async set(key: string, value: any) { async set(key: string, value: any) {
if (!this.cache) { if (!this.cache) {
return; return;
@@ -28,6 +45,16 @@ export class CacheService {
this.cache.set(key, value); this.cache.set(key, value);
} }
public async hSet(key: string, field: string, value: any) {
try {
const json = JSON.stringify(value, BufferJSON.replacer);
await this.cache.hSet(key, field, json);
} catch (error) {
this.logger.error(error);
}
}
async has(key: string) { async has(key: string) {
if (!this.cache) { if (!this.cache) {
return; return;
@@ -44,6 +71,16 @@ export class CacheService {
return this.cache.delete(key); return this.cache.delete(key);
} }
async hDelete(key: string, field: string) {
try {
await this.cache.hDelete(key, field);
return true;
} catch (error) {
this.logger.error(error);
return false;
}
}
async deleteAll(appendCriteria?: string) { async deleteAll(appendCriteria?: string) {
if (!this.cache) { if (!this.cache) {
return; return;

View File

@@ -13,6 +13,7 @@ import {
Database, Database,
HttpServer, HttpServer,
Log, Log,
Rabbitmq,
Sqs, Sqs,
Webhook, Webhook,
Websocket, Websocket,
@@ -38,17 +39,17 @@ import { waMonitor } from '../server.module';
import { Events, wa } from '../types/wa.types'; import { Events, wa } from '../types/wa.types';
import { CacheService } from './cache.service'; import { CacheService } from './cache.service';
export class WAStartupService { export class ChannelStartupService {
constructor( constructor(
public readonly configService: ConfigService, public readonly configService: ConfigService,
public readonly eventEmitter: EventEmitter2, public readonly eventEmitter: EventEmitter2,
public readonly repository: RepositoryBroker, public readonly repository: RepositoryBroker,
public readonly chatwootCache: CacheService, public readonly chatwootCache: CacheService,
) { ) {
this.logger.verbose('WAStartupService initialized'); this.logger.verbose('ChannelStartupService initialized');
} }
public readonly logger = new Logger(WAStartupService.name); public readonly logger = new Logger(ChannelStartupService.name);
public client: WASocket; public client: WASocket;
public readonly instance: wa.Instance = {}; public readonly instance: wa.Instance = {};
@@ -71,6 +72,8 @@ export class WAStartupService {
public chamaaiService = new ChamaaiService(waMonitor, this.configService); public chamaaiService = new ChamaaiService(waMonitor, this.configService);
public set instanceName(name: string) { public set instanceName(name: string) {
this.logger.setInstance(name);
this.logger.verbose(`Initializing instance '${name}'`); this.logger.verbose(`Initializing instance '${name}'`);
if (!name) { if (!name) {
this.logger.verbose('Instance name not found, generating random name with uuid'); this.logger.verbose('Instance name not found, generating random name with uuid');
@@ -134,11 +137,13 @@ export class WAStartupService {
public async findIntegration() { public async findIntegration() {
this.logger.verbose('Finding integration'); this.logger.verbose('Finding integration');
const data = await this.repository.integration.find(this.instanceName); let data: any;
data = await this.repository.integration.find(this.instanceName);
if (!data) { if (!data) {
this.logger.verbose('Integration not found'); this.repository.integration.create({ integration: 'WHATSAPP-BAILEYS', number: '', token: '' }, this.instanceName);
throw new NotFoundException('Integration not found'); data = { integration: 'WHATSAPP-BAILEYS', number: '', token: '' };
} }
this.logger.verbose(`Integration: ${data.integration}`); this.logger.verbose(`Integration: ${data.integration}`);
@@ -301,6 +306,9 @@ export class WAStartupService {
this.localChatwoot.conversation_pending = data?.conversation_pending; this.localChatwoot.conversation_pending = data?.conversation_pending;
this.logger.verbose(`Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`); this.logger.verbose(`Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`);
this.localChatwoot.merge_brazil_contacts = data?.merge_brazil_contacts;
this.logger.verbose(`Chatwoot merge brazil contacts: ${this.localChatwoot.merge_brazil_contacts}`);
this.localChatwoot.import_contacts = data?.import_contacts; this.localChatwoot.import_contacts = data?.import_contacts;
this.logger.verbose(`Chatwoot import contacts: ${this.localChatwoot.import_contacts}`); this.logger.verbose(`Chatwoot import contacts: ${this.localChatwoot.import_contacts}`);
@@ -324,6 +332,7 @@ export class WAStartupService {
this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`); this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`);
this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`);
this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`);
this.logger.verbose(`Chatwoot merge brazil contacts: ${data.merge_brazil_contacts}`);
this.logger.verbose(`Chatwoot import contacts: ${data.import_contacts}`); this.logger.verbose(`Chatwoot import contacts: ${data.import_contacts}`);
this.logger.verbose(`Chatwoot import messages: ${data.import_messages}`); this.logger.verbose(`Chatwoot import messages: ${data.import_messages}`);
this.logger.verbose(`Chatwoot days limit import messages: ${data.days_limit_import_messages}`); this.logger.verbose(`Chatwoot days limit import messages: ${data.days_limit_import_messages}`);
@@ -352,6 +361,7 @@ export class WAStartupService {
this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`); this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`);
this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`);
this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`);
this.logger.verbose(`Chatwoot merge brazilian contacts: ${data.merge_brazil_contacts}`);
this.logger.verbose(`Chatwoot import contacts: ${data.import_contacts}`); this.logger.verbose(`Chatwoot import contacts: ${data.import_contacts}`);
this.logger.verbose(`Chatwoot import messages: ${data.import_messages}`); this.logger.verbose(`Chatwoot import messages: ${data.import_messages}`);
this.logger.verbose(`Chatwoot days limit import messages: ${data.days_limit_import_messages}`); this.logger.verbose(`Chatwoot days limit import messages: ${data.days_limit_import_messages}`);
@@ -366,6 +376,7 @@ export class WAStartupService {
sign_delimiter: data.sign_delimiter || null, sign_delimiter: data.sign_delimiter || null,
reopen_conversation: data.reopen_conversation, reopen_conversation: data.reopen_conversation,
conversation_pending: data.conversation_pending, conversation_pending: data.conversation_pending,
merge_brazil_contacts: data.merge_brazil_contacts,
import_contacts: data.import_contacts, import_contacts: data.import_contacts,
import_messages: data.import_messages, import_messages: data.import_messages,
days_limit_import_messages: data.days_limit_import_messages, days_limit_import_messages: data.days_limit_import_messages,
@@ -583,7 +594,7 @@ export class WAStartupService {
this.logger.verbose(`Proxy enabled: ${this.localProxy.enabled}`); this.logger.verbose(`Proxy enabled: ${this.localProxy.enabled}`);
this.localProxy.proxy = data?.proxy; this.localProxy.proxy = data?.proxy;
this.logger.verbose(`Proxy proxy: ${this.localProxy.proxy.host}`); this.logger.verbose(`Proxy proxy: ${this.localProxy.proxy?.host}`);
this.logger.verbose('Proxy loaded'); this.logger.verbose('Proxy loaded');
} }
@@ -682,6 +693,9 @@ export class WAStartupService {
const rabbitmqLocal = this.localRabbitmq.events; const rabbitmqLocal = this.localRabbitmq.events;
const sqsLocal = this.localSqs.events; const sqsLocal = this.localSqs.events;
const serverUrl = this.configService.get<HttpServer>('SERVER').URL; const serverUrl = this.configService.get<HttpServer>('SERVER').URL;
const rabbitmqEnabled = this.configService.get<Rabbitmq>('RABBITMQ').ENABLED;
const rabbitmqGlobal = this.configService.get<Rabbitmq>('RABBITMQ').GLOBAL_ENABLED;
const rabbitmqEvents = this.configService.get<Rabbitmq>('RABBITMQ').EVENTS;
const we = event.replace(/[.-]/gm, '_').toUpperCase(); const we = event.replace(/[.-]/gm, '_').toUpperCase();
const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase();
const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds
@@ -692,67 +706,134 @@ export class WAStartupService {
const tokenStore = await this.repository.auth.find(this.instanceName); const tokenStore = await this.repository.auth.find(this.instanceName);
const instanceApikey = tokenStore?.apikey || 'Apikey not found'; const instanceApikey = tokenStore?.apikey || 'Apikey not found';
if (this.localRabbitmq.enabled) { if (rabbitmqEnabled) {
const amqp = getAMQP(); const amqp = getAMQP();
if (this.localRabbitmq.enabled && amqp) {
if (amqp) {
if (Array.isArray(rabbitmqLocal) && rabbitmqLocal.includes(we)) { if (Array.isArray(rabbitmqLocal) && rabbitmqLocal.includes(we)) {
const exchangeName = this.instanceName ?? 'evolution_exchange'; const exchangeName = this.instanceName ?? 'evolution_exchange';
// await amqp.assertExchange(exchangeName, 'topic', { let retry = 0;
// durable: true,
// autoDelete: false,
// });
await this.assertExchangeAsync(amqp, exchangeName, 'topic', { while (retry < 3) {
durable: true, try {
autoDelete: false, await amqp.assertExchange(exchangeName, 'topic', {
}); durable: true,
autoDelete: false,
});
const queueName = `${this.instanceName}.${event}`; const queueName = `${this.instanceName}.${event}`;
await amqp.assertQueue(queueName, { await amqp.assertQueue(queueName, {
durable: true, durable: true,
autoDelete: false, autoDelete: false,
arguments: { arguments: {
'x-queue-type': 'quorum', 'x-queue-type': 'quorum',
}, },
}); });
await amqp.bindQueue(queueName, exchangeName, event); await amqp.bindQueue(queueName, exchangeName, event);
const message = { const message = {
event, event,
instance: this.instance.name, instance: this.instance.name,
data, data,
server_url: serverUrl, server_url: serverUrl,
date_time: now, date_time: now,
sender: this.wuid, sender: this.wuid,
}; };
if (expose && instanceApikey) { if (expose && instanceApikey) {
message['apikey'] = instanceApikey; message['apikey'] = instanceApikey;
}
await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message)));
if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) {
const logData = {
local: ChannelStartupService.name + '.sendData-RabbitMQ',
event,
instance: this.instance.name,
data,
server_url: serverUrl,
apikey: (expose && instanceApikey) || null,
date_time: now,
sender: this.wuid,
};
if (expose && instanceApikey) {
logData['apikey'] = instanceApikey;
}
this.logger.log(logData);
}
break;
} catch (error) {
retry++;
}
} }
}
}
await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message))); if (rabbitmqGlobal && rabbitmqEvents[we] && amqp) {
const exchangeName = 'evolution_exchange';
if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) { let retry = 0;
const logData = {
local: WAStartupService.name + '.sendData-RabbitMQ', while (retry < 3) {
try {
await amqp.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
const queueName = transformedWe;
await amqp.assertQueue(queueName, {
durable: true,
autoDelete: false,
arguments: {
'x-queue-type': 'quorum',
},
});
await amqp.bindQueue(queueName, exchangeName, event);
const message = {
event, event,
instance: this.instance.name, instance: this.instance.name,
data, data,
server_url: serverUrl, server_url: serverUrl,
apikey: (expose && instanceApikey) || null,
date_time: now, date_time: now,
sender: this.wuid, sender: this.wuid,
}; };
if (expose && instanceApikey) { if (expose && instanceApikey) {
logData['apikey'] = instanceApikey; message['apikey'] = instanceApikey;
}
await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message)));
if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) {
const logData = {
local: ChannelStartupService.name + '.sendData-RabbitMQ-Global',
event,
instance: this.instance.name,
data,
server_url: serverUrl,
apikey: (expose && instanceApikey) || null,
date_time: now,
sender: this.wuid,
};
if (expose && instanceApikey) {
logData['apikey'] = instanceApikey;
}
this.logger.log(logData);
} }
this.logger.log(logData); break;
} catch (error) {
retry++;
} }
} }
} }
@@ -794,7 +875,7 @@ export class WAStartupService {
sqs.sendMessage(params, (err, data) => { sqs.sendMessage(params, (err, data) => {
if (err) { if (err) {
this.logger.error({ this.logger.error({
local: WAStartupService.name + '.sendData-SQS', local: ChannelStartupService.name + '.sendData-SQS',
message: err?.message, message: err?.message,
hostName: err?.hostname, hostName: err?.hostname,
code: err?.code, code: err?.code,
@@ -806,7 +887,7 @@ export class WAStartupService {
} else { } else {
if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) { if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) {
const logData = { const logData = {
local: WAStartupService.name + '.sendData-SQS', local: ChannelStartupService.name + '.sendData-SQS',
event, event,
instance: this.instance.name, instance: this.instance.name,
data, data,
@@ -850,7 +931,7 @@ export class WAStartupService {
if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) { if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) {
const logData = { const logData = {
local: WAStartupService.name + '.sendData-WebsocketGlobal', local: ChannelStartupService.name + '.sendData-WebsocketGlobal',
event, event,
instance: this.instance.name, instance: this.instance.name,
data, data,
@@ -880,7 +961,7 @@ export class WAStartupService {
if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) { if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) {
const logData = { const logData = {
local: WAStartupService.name + '.sendData-Websocket', local: ChannelStartupService.name + '.sendData-Websocket',
event, event,
instance: this.instance.name, instance: this.instance.name,
data, data,
@@ -914,7 +995,7 @@ export class WAStartupService {
if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) { if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) {
const logData = { const logData = {
local: WAStartupService.name + '.sendDataWebhook-local', local: ChannelStartupService.name + '.sendDataWebhook-local',
url: baseURL, url: baseURL,
event, event,
instance: this.instance.name, instance: this.instance.name,
@@ -954,7 +1035,7 @@ export class WAStartupService {
} }
} catch (error) { } catch (error) {
this.logger.error({ this.logger.error({
local: WAStartupService.name + '.sendDataWebhook-local', local: ChannelStartupService.name + '.sendDataWebhook-local',
message: error?.message, message: error?.message,
hostName: error?.hostname, hostName: error?.hostname,
syscall: error?.syscall, syscall: error?.syscall,
@@ -986,7 +1067,7 @@ export class WAStartupService {
if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) { if (this.configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) {
const logData = { const logData = {
local: WAStartupService.name + '.sendDataWebhook-global', local: ChannelStartupService.name + '.sendDataWebhook-global',
url: globalURL, url: globalURL,
event, event,
instance: this.instance.name, instance: this.instance.name,
@@ -1025,7 +1106,7 @@ export class WAStartupService {
} }
} catch (error) { } catch (error) {
this.logger.error({ this.logger.error({
local: WAStartupService.name + '.sendDataWebhook-global', local: ChannelStartupService.name + '.sendDataWebhook-global',
message: error?.message, message: error?.message,
hostName: error?.hostname, hostName: error?.hostname,
syscall: error?.syscall, syscall: error?.syscall,

View File

@@ -39,9 +39,10 @@ import makeWASocket, {
import { Label } from '@whiskeysockets/baileys/lib/Types/Label'; import { Label } from '@whiskeysockets/baileys/lib/Types/Label';
import { LabelAssociation } from '@whiskeysockets/baileys/lib/Types/LabelAssociation'; import { LabelAssociation } from '@whiskeysockets/baileys/lib/Types/LabelAssociation';
import axios from 'axios'; import axios from 'axios';
import { arrayUnique, isBase64, isURL } from 'class-validator'; import { exec } from 'child_process';
import { isBase64, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import ffmpeg from 'fluent-ffmpeg'; // import ffmpeg from 'fluent-ffmpeg';
import fs, { existsSync, readFileSync } from 'fs'; import fs, { existsSync, readFileSync } from 'fs';
import { parsePhoneNumber } from 'libphonenumber-js'; import { parsePhoneNumber } from 'libphonenumber-js';
import Long from 'long'; import Long from 'long';
@@ -54,13 +55,23 @@ import qrcode, { QRCodeToDataURLOptions } from 'qrcode';
import qrcodeTerminal from 'qrcode-terminal'; import qrcodeTerminal from 'qrcode-terminal';
import sharp from 'sharp'; import sharp from 'sharp';
import { ConfigService, ConfigSessionPhone, Database, Log, QrCode, Redis } from '../../../config/env.config'; import { CacheEngine } from '../../../cache/cacheengine';
import {
CacheConf,
ConfigService,
configService,
ConfigSessionPhone,
Database,
Log,
ProviderSession,
QrCode,
} from '../../../config/env.config';
import { INSTANCE_DIR } from '../../../config/path.config'; import { INSTANCE_DIR } from '../../../config/path.config';
import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../../exceptions'; import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../../exceptions';
import { dbserver } from '../../../libs/db.connect'; import { dbserver } from '../../../libs/db.connect';
import { RedisCache } from '../../../libs/redis.client';
import { makeProxyAgent } from '../../../utils/makeProxyAgent'; import { makeProxyAgent } from '../../../utils/makeProxyAgent';
import { useMultiFileAuthStateDb } from '../../../utils/use-multi-file-auth-state-db'; import { useMultiFileAuthStateDb } from '../../../utils/use-multi-file-auth-state-db';
import { AuthStateProvider } from '../../../utils/use-multi-file-auth-state-provider-files';
import { useMultiFileAuthStateRedisDb } from '../../../utils/use-multi-file-auth-state-redis-db'; import { useMultiFileAuthStateRedisDb } from '../../../utils/use-multi-file-auth-state-redis-db';
import { import {
ArchiveChatDto, ArchiveChatDto,
@@ -68,6 +79,7 @@ import {
DeleteMessage, DeleteMessage,
getBase64FromMediaMessageDto, getBase64FromMediaMessageDto,
LastMessage, LastMessage,
MarkChatUnreadDto,
NumberBusiness, NumberBusiness,
OnWhatsAppDto, OnWhatsAppDto,
PrivacySettingDto, PrivacySettingDto,
@@ -97,7 +109,6 @@ import {
MediaMessage, MediaMessage,
Options, Options,
SendAudioDto, SendAudioDto,
SendButtonDto,
SendContactDto, SendContactDto,
SendListDto, SendListDto,
SendLocationDto, SendLocationDto,
@@ -114,29 +125,37 @@ import { SettingsRaw } from '../../models';
import { ChatRaw } from '../../models/chat.model'; import { ChatRaw } from '../../models/chat.model';
import { ContactRaw } from '../../models/contact.model'; import { ContactRaw } from '../../models/contact.model';
import { MessageRaw, MessageUpdateRaw } from '../../models/message.model'; import { MessageRaw, MessageUpdateRaw } from '../../models/message.model';
import { ProviderFiles } from '../../provider/sessions';
import { RepositoryBroker } from '../../repository/repository.manager'; import { RepositoryBroker } from '../../repository/repository.manager';
import { waMonitor } from '../../server.module'; import { waMonitor } from '../../server.module';
import { Events, MessageSubtype, TypeMediaMessage, wa } from '../../types/wa.types'; import { Events, MessageSubtype, TypeMediaMessage, wa } from '../../types/wa.types';
import { CacheService } from './../cache.service'; import { CacheService } from './../cache.service';
import { WAStartupService } from './../whatsapp.service'; import { ChannelStartupService } from './../channel.service';
// const retryCache = {}; const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
export class BaileysStartupService extends WAStartupService { export class BaileysStartupService extends ChannelStartupService {
constructor( constructor(
public readonly configService: ConfigService, public readonly configService: ConfigService,
public readonly eventEmitter: EventEmitter2, public readonly eventEmitter: EventEmitter2,
public readonly repository: RepositoryBroker, public readonly repository: RepositoryBroker,
public readonly cache: RedisCache, public readonly cache: CacheService,
public readonly chatwootCache: CacheService, public readonly chatwootCache: CacheService,
public readonly baileysCache: CacheService,
private readonly providerFiles: ProviderFiles,
) { ) {
super(configService, eventEmitter, repository, chatwootCache); super(configService, eventEmitter, repository, chatwootCache);
this.logger.verbose('BaileysStartupService initialized'); this.logger.verbose('BaileysStartupService initialized');
this.cleanStore(); this.cleanStore();
this.instance.qrcode = { count: 0 }; this.instance.qrcode = { count: 0 };
this.mobile = false; this.mobile = false;
this.recoveringMessages();
this.forceUpdateGroupMetadataCache();
this.authStateProvider = new AuthStateProvider(this.providerFiles);
} }
private authStateProvider: AuthStateProvider;
private readonly msgRetryCounterCache: CacheStore = new NodeCache(); private readonly msgRetryCounterCache: CacheStore = new NodeCache();
private readonly userDevicesCache: CacheStore = new NodeCache(); private readonly userDevicesCache: CacheStore = new NodeCache();
private endSession = false; private endSession = false;
@@ -147,6 +166,43 @@ export class BaileysStartupService extends WAStartupService {
public phoneNumber: string; public phoneNumber: string;
public mobile: boolean; public mobile: boolean;
private async recoveringMessages() {
this.logger.info('Recovering messages lost');
const cacheConf = this.configService.get<CacheConf>('CACHE');
if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) {
setInterval(async () => {
this.baileysCache.keys().then((keys) => {
keys.forEach(async (key) => {
const message = await this.baileysCache.get(key.split(':')[2]);
if (message.messageStubParameters && message.messageStubParameters[0] === 'Message absent from node') {
this.logger.info('Message absent from node, retrying to send, key: ' + key.split(':')[2]);
await this.client.sendMessageAck(JSON.parse(message.messageStubParameters[1], BufferJSON.reviver));
}
});
});
}, 30000);
}
}
private async forceUpdateGroupMetadataCache() {
if (
!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED
)
return;
setInterval(async () => {
this.logger.verbose('Forcing update group metadata cache');
const groups = await this.fetchAllGroups({ getParticipants: 'false' });
for (const group of groups) {
await this.updateGroupMetadataCache(group.id);
}
}, 3600000);
}
public get connectionStatus() { public get connectionStatus() {
this.logger.verbose('Getting connection status'); this.logger.verbose('Getting connection status');
return this.stateConnection; return this.stateConnection;
@@ -377,10 +433,12 @@ export class BaileysStartupService extends WAStartupService {
CONNECTED TO WHATSAPP CONNECTED TO WHATSAPP
`.replace(/^ +/gm, ' '), `.replace(/^ +/gm, ' '),
); );
this.logger.info(` this.logger.info(
`
wuid: ${formattedWuid} wuid: ${formattedWuid}
name: ${formattedName} name: ${formattedName}
`); `,
);
if (this.localChatwoot.enabled) { if (this.localChatwoot.enabled) {
this.chatwootService.eventWhatsapp( this.chatwootService.eventWhatsapp(
@@ -437,12 +495,17 @@ export class BaileysStartupService extends WAStartupService {
private async defineAuthState() { private async defineAuthState() {
this.logger.verbose('Defining auth state'); this.logger.verbose('Defining auth state');
const db = this.configService.get<Database>('DATABASE'); const db = this.configService.get<Database>('DATABASE');
const redis = this.configService.get<Redis>('REDIS'); const cache = this.configService.get<CacheConf>('CACHE');
if (redis?.ENABLED) { const provider = this.configService.get<ProviderSession>('PROVIDER');
this.logger.verbose('Redis enabled');
this.cache.reference = this.instance.name; if (provider?.ENABLED) {
return await useMultiFileAuthStateRedisDb(this.cache); return await this.authStateProvider.authStateProvider(this.instance.name);
}
if (cache?.REDIS.ENABLED && cache?.REDIS.SAVE_INSTANCES) {
this.logger.info('Redis enabled');
return await useMultiFileAuthStateRedisDb(this.instance.name, this.cache);
} }
if (db.SAVE_DATA.INSTANCE && db.ENABLED) { if (db.SAVE_DATA.INSTANCE && db.ENABLED) {
@@ -475,27 +538,39 @@ export class BaileysStartupService extends WAStartupService {
this.mobile = mobile; this.mobile = mobile;
} }
const { version } = await fetchLatestBaileysVersion();
this.logger.verbose('Baileys version: ' + version);
const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE'); const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE');
const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()];
this.logger.verbose('Browser: ' + JSON.stringify(browser)); this.logger.verbose('Browser: ' + JSON.stringify(browser));
let version;
let log;
if (session.VERSION) {
version = session.VERSION.split(',');
log = `Baileys version env: ${version}`;
} else {
const baileysVersion = await fetchLatestBaileysVersion();
version = baileysVersion.version;
log = `Baileys version: ${version}`;
}
this.logger.info(log);
let options; let options;
if (this.localProxy.enabled) { if (this.localProxy.enabled) {
this.logger.info('Proxy enabled: ' + this.localProxy.proxy.host); this.logger.info('Proxy enabled: ' + this.localProxy.proxy?.host);
if (this.localProxy?.proxy?.host?.includes('proxyscrape')) { if (this.localProxy?.proxy?.host?.includes('proxyscrape')) {
try { try {
const response = await axios.get(this.localProxy.proxy.host); const response = await axios.get(this.localProxy.proxy?.host);
const text = response.data; const text = response.data;
const proxyUrls = text.split('\r\n'); const proxyUrls = text.split('\r\n');
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
const proxyUrl = 'http://' + proxyUrls[rand]; const proxyUrl = 'http://' + proxyUrls[rand];
options = { options = {
agent: makeProxyAgent(proxyUrl), agent: makeProxyAgent(proxyUrl),
fetchAgent: makeProxyAgent(proxyUrl),
}; };
} catch (error) { } catch (error) {
this.localProxy.enabled = false; this.localProxy.enabled = false;
@@ -503,6 +578,7 @@ export class BaileysStartupService extends WAStartupService {
} else { } else {
options = { options = {
agent: makeProxyAgent(this.localProxy.proxy), agent: makeProxyAgent(this.localProxy.proxy),
fetchAgent: makeProxyAgent(this.localProxy.proxy),
}; };
} }
} }
@@ -519,9 +595,12 @@ export class BaileysStartupService extends WAStartupService {
browser: number ? ['Chrome (Linux)', session.NAME, release()] : browser, browser: number ? ['Chrome (Linux)', session.NAME, release()] : browser,
version, version,
markOnlineOnConnect: this.localSettings.always_online, markOnlineOnConnect: this.localSettings.always_online,
retryRequestDelayMs: 10, retryRequestDelayMs: 350,
connectTimeoutMs: 60_000, maxMsgRetryCount: 4,
qrTimeout: 40_000, fireInitQueries: true,
connectTimeoutMs: 20_000,
keepAliveIntervalMs: 30_000,
qrTimeout: 45_000,
defaultQueryTimeoutMs: undefined, defaultQueryTimeoutMs: undefined,
emitOwnEvents: false, emitOwnEvents: false,
shouldIgnoreJid: (jid) => { shouldIgnoreJid: (jid) => {
@@ -538,7 +617,7 @@ export class BaileysStartupService extends WAStartupService {
return this.historySyncNotification(msg); return this.historySyncNotification(msg);
}, },
userDevicesCache: this.userDevicesCache, userDevicesCache: this.userDevicesCache,
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, transactionOpts: { maxCommitRetries: 5, delayBetweenTriesMs: 2500 },
patchMessageBeforeSending(message) { patchMessageBeforeSending(message) {
if ( if (
message.deviceSentMessage?.message?.listMessage?.listType === message.deviceSentMessage?.message?.listMessage?.listType ===
@@ -594,12 +673,8 @@ export class BaileysStartupService extends WAStartupService {
return; return;
} }
console.log('phoneNumber', phoneNumber);
const parsedPhoneNumber = parsePhoneNumber(phoneNumber); const parsedPhoneNumber = parsePhoneNumber(phoneNumber);
console.log('parsedPhoneNumber', parsedPhoneNumber);
if (!parsedPhoneNumber?.isValid()) { if (!parsedPhoneNumber?.isValid()) {
this.logger.error('Phone number invalid'); this.logger.error('Phone number invalid');
return; return;
@@ -621,7 +696,6 @@ export class BaileysStartupService extends WAStartupService {
try { try {
const response = await this.client.requestRegistrationCode(registration); const response = await this.client.requestRegistrationCode(registration);
console.log('response', response);
if (['ok', 'sent'].includes(response?.status)) { if (['ok', 'sent'].includes(response?.status)) {
this.logger.verbose('Registration code sent successfully'); this.logger.verbose('Registration code sent successfully');
@@ -635,9 +709,8 @@ export class BaileysStartupService extends WAStartupService {
public async receiveMobileCode(code: string) { public async receiveMobileCode(code: string) {
await this.client await this.client
.register(code.replace(/["']/g, '').trim().toLowerCase()) .register(code.replace(/["']/g, '').trim().toLowerCase())
.then(async (response) => { .then(async () => {
this.logger.verbose('Registration code received successfully'); this.logger.verbose('Registration code received successfully');
console.log(response);
}) })
.catch((error) => { .catch((error) => {
this.logger.error(error); this.logger.error(error);
@@ -648,24 +721,38 @@ export class BaileysStartupService extends WAStartupService {
try { try {
this.instance.authState = await this.defineAuthState(); this.instance.authState = await this.defineAuthState();
const { version } = await fetchLatestBaileysVersion();
const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE'); const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE');
const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()];
let version;
let log;
if (session.VERSION) {
version = session.VERSION.split(',');
log = `Baileys version env: ${version}`;
} else {
const baileysVersion = await fetchLatestBaileysVersion();
version = baileysVersion.version;
log = `Baileys version: ${version}`;
}
this.logger.info(log);
let options; let options;
if (this.localProxy.enabled) { if (this.localProxy.enabled) {
this.logger.info('Proxy enabled: ' + this.localProxy.proxy.host); this.logger.info('Proxy enabled: ' + this.localProxy.proxy?.host);
if (this.localProxy?.proxy?.host?.includes('proxyscrape')) { if (this.localProxy?.proxy?.host?.includes('proxyscrape')) {
try { try {
const response = await axios.get(this.localProxy.proxy.host); const response = await axios.get(this.localProxy.proxy?.host);
const text = response.data; const text = response.data;
const proxyUrls = text.split('\r\n'); const proxyUrls = text.split('\r\n');
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
const proxyUrl = 'http://' + proxyUrls[rand]; const proxyUrl = 'http://' + proxyUrls[rand];
options = { options = {
agent: makeProxyAgent(proxyUrl), agent: makeProxyAgent(proxyUrl),
fetchAgent: makeProxyAgent(proxyUrl),
}; };
} catch (error) { } catch (error) {
this.localProxy.enabled = false; this.localProxy.enabled = false;
@@ -673,6 +760,7 @@ export class BaileysStartupService extends WAStartupService {
} else { } else {
options = { options = {
agent: makeProxyAgent(this.localProxy.proxy), agent: makeProxyAgent(this.localProxy.proxy),
fetchAgent: makeProxyAgent(this.localProxy.proxy),
}; };
} }
} }
@@ -685,12 +773,16 @@ export class BaileysStartupService extends WAStartupService {
}, },
logger: P({ level: this.logBaileys }), logger: P({ level: this.logBaileys }),
printQRInTerminal: false, printQRInTerminal: false,
mobile: this.mobile,
browser: this.phoneNumber ? ['Chrome (Linux)', session.NAME, release()] : browser, browser: this.phoneNumber ? ['Chrome (Linux)', session.NAME, release()] : browser,
version, version,
markOnlineOnConnect: this.localSettings.always_online, markOnlineOnConnect: this.localSettings.always_online,
retryRequestDelayMs: 10, retryRequestDelayMs: 350,
connectTimeoutMs: 60_000, maxMsgRetryCount: 4,
qrTimeout: 40_000, fireInitQueries: true,
connectTimeoutMs: 20_000,
keepAliveIntervalMs: 30_000,
qrTimeout: 45_000,
defaultQueryTimeoutMs: undefined, defaultQueryTimeoutMs: undefined,
emitOwnEvents: false, emitOwnEvents: false,
shouldIgnoreJid: (jid) => { shouldIgnoreJid: (jid) => {
@@ -707,7 +799,7 @@ export class BaileysStartupService extends WAStartupService {
return this.historySyncNotification(msg); return this.historySyncNotification(msg);
}, },
userDevicesCache: this.userDevicesCache, userDevicesCache: this.userDevicesCache,
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, transactionOpts: { maxCommitRetries: 5, delayBetweenTriesMs: 2500 },
patchMessageBeforeSending(message) { patchMessageBeforeSending(message) {
if ( if (
message.deviceSentMessage?.message?.listMessage?.listType === message.deviceSentMessage?.message?.listMessage?.listType ===
@@ -976,6 +1068,15 @@ export class BaileysStartupService extends WAStartupService {
continue; continue;
} }
const status: Record<number, wa.StatusMessage> = {
0: 'ERROR',
1: 'PENDING',
2: 'SERVER_ACK',
3: 'DELIVERY_ACK',
4: 'READ',
5: 'PLAYED',
};
messagesRaw.push({ messagesRaw.push({
key: m.key, key: m.key,
pushName: m.pushName || m.key.remoteJid.split('@')[0], pushName: m.pushName || m.key.remoteJid.split('@')[0],
@@ -984,6 +1085,7 @@ export class BaileysStartupService extends WAStartupService {
messageType: getContentType(m.message), messageType: getContentType(m.message),
messageTimestamp: m.messageTimestamp as number, messageTimestamp: m.messageTimestamp as number,
owner: this.instance.name, owner: this.instance.name,
status: m.status ? status[m.status] : null,
}); });
} }
@@ -1043,6 +1145,20 @@ export class BaileysStartupService extends WAStartupService {
} }
} }
if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') {
this.logger.info('Recovering message lost');
await this.baileysCache.set(received.key.id, received);
continue;
}
const retryCache = (await this.baileysCache.get(received.key.id)) || null;
if (retryCache) {
this.logger.info('Recovered message lost');
await this.baileysCache.delete(received.key.id);
}
if ( if (
(type !== 'notify' && type !== 'append') || (type !== 'notify' && type !== 'append') ||
received.message?.protocolMessage || received.message?.protocolMessage ||
@@ -1241,7 +1357,6 @@ export class BaileysStartupService extends WAStartupService {
} }
} }
// if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) {
if (key.remoteJid !== 'status@broadcast') { if (key.remoteJid !== 'status@broadcast') {
this.logger.verbose('Message update is valid'); this.logger.verbose('Message update is valid');
@@ -1330,6 +1445,12 @@ export class BaileysStartupService extends WAStartupService {
this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE');
this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate);
groupMetadataUpdate.forEach((group) => {
if (isJidGroup(group.id)) {
this.updateGroupMetadataCache(group.id);
}
});
}, },
'group-participants.update': (participantsUpdate: { 'group-participants.update': (participantsUpdate: {
@@ -1387,7 +1508,7 @@ export class BaileysStartupService extends WAStartupService {
this.logger.verbose('Sending data to webhook in event LABELS_ASSOCIATION'); this.logger.verbose('Sending data to webhook in event LABELS_ASSOCIATION');
// Atualiza labels nos chats // Atualiza labels nos chats
if (database.SAVE_DATA.CHATS) { if (database.ENABLED && database.SAVE_DATA.CHATS) {
const chats = await this.repository.chat.find({ const chats = await this.repository.chat.find({
where: { where: {
owner: this.instance.name, owner: this.instance.name,
@@ -1472,27 +1593,12 @@ export class BaileysStartupService extends WAStartupService {
if (events['messages.upsert']) { if (events['messages.upsert']) {
this.logger.verbose('Listening event: messages.upsert'); this.logger.verbose('Listening event: messages.upsert');
const payload = events['messages.upsert']; const payload = events['messages.upsert'];
// if (payload.messages.find((a) => a?.messageStubType === 2)) {
// const msg = payload.messages[0];
// retryCache[msg.key.id] = msg;
// return;
// }
this.messageHandle['messages.upsert'](payload, database, settings); this.messageHandle['messages.upsert'](payload, database, settings);
} }
if (events['messages.update']) { if (events['messages.update']) {
this.logger.verbose('Listening event: messages.update'); this.logger.verbose('Listening event: messages.update');
const payload = events['messages.update']; const payload = events['messages.update'];
// payload.forEach((message) => {
// if (retryCache[message.key.id]) {
// this.client.ev.emit('messages.upsert', {
// messages: [message],
// type: 'notify',
// });
// delete retryCache[message.key.id];
// return;
// }
// });
this.messageHandle['messages.update'](payload, database, settings); this.messageHandle['messages.update'](payload, database, settings);
} }
@@ -1781,7 +1887,11 @@ export class BaileysStartupService extends WAStartupService {
let mentions: string[]; let mentions: string[];
if (isJidGroup(sender)) { if (isJidGroup(sender)) {
try { try {
const group = await this.findGroup({ groupJid: sender }, 'inner'); let group;
const cache = this.configService.get<CacheConf>('CACHE');
if (!cache.REDIS.ENABLED && !cache.LOCAL.ENABLED) group = await this.findGroup({ groupJid: sender }, 'inner');
else group = await this.getGroupMetadataCache(sender);
if (!group) { if (!group) {
throw new NotFoundException('Group not found'); throw new NotFoundException('Group not found');
@@ -1834,7 +1944,14 @@ export class BaileysStartupService extends WAStartupService {
key: message['reactionMessage']['key'], key: message['reactionMessage']['key'],
}, },
} as unknown as AnyMessageContent, } as unknown as AnyMessageContent,
option as unknown as MiscMessageGenerationOptions, {
...option,
cachedGroupMetadata:
!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED
? null
: this.getGroupMetadataCache,
} as unknown as MiscMessageGenerationOptions,
); );
} }
} }
@@ -1847,7 +1964,14 @@ export class BaileysStartupService extends WAStartupService {
mentions, mentions,
linkPreview: linkPreview, linkPreview: linkPreview,
} as unknown as AnyMessageContent, } as unknown as AnyMessageContent,
option as unknown as MiscMessageGenerationOptions, {
...option,
cachedGroupMetadata:
!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED
? null
: this.getGroupMetadataCache,
} as unknown as MiscMessageGenerationOptions,
); );
} }
@@ -1862,7 +1986,14 @@ export class BaileysStartupService extends WAStartupService {
}, },
mentions, mentions,
}, },
option as unknown as MiscMessageGenerationOptions, {
...option,
cachedGroupMetadata:
!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED
? null
: this.getGroupMetadataCache,
} as unknown as MiscMessageGenerationOptions,
); );
} }
@@ -1883,7 +2014,14 @@ export class BaileysStartupService extends WAStartupService {
return await this.client.sendMessage( return await this.client.sendMessage(
sender, sender,
message as unknown as AnyMessageContent, message as unknown as AnyMessageContent,
option as unknown as MiscMessageGenerationOptions, {
...option,
cachedGroupMetadata:
!this.configService.get<CacheConf>('CACHE').REDIS.ENABLED &&
!this.configService.get<CacheConf>('CACHE').LOCAL.ENABLED
? null
: this.getGroupMetadataCache,
} as unknown as MiscMessageGenerationOptions,
); );
})(); })();
@@ -1938,18 +2076,37 @@ export class BaileysStartupService extends WAStartupService {
const sender = isWA.jid; const sender = isWA.jid;
this.logger.verbose('Sending presence'); if (data?.options?.delay && data?.options?.delay > 20000) {
await this.client.presenceSubscribe(sender); let remainingDelay = data?.options.delay;
this.logger.verbose('Subscribing to presence'); while (remainingDelay > 20000) {
await this.client.presenceSubscribe(sender);
await this.client.sendPresenceUpdate(data.options?.presence ?? 'composing', sender); await this.client.sendPresenceUpdate((data?.options?.presence as WAPresence) ?? 'composing', sender);
this.logger.verbose('Sending presence update: ' + data.options?.presence ?? 'composing');
await delay(data.options.delay); await delay(20000);
this.logger.verbose('Set delay: ' + data.options.delay);
await this.client.sendPresenceUpdate('paused', sender); await this.client.sendPresenceUpdate('paused', sender);
this.logger.verbose('Sending presence update: paused');
remainingDelay -= 20000;
}
if (remainingDelay > 0) {
await this.client.presenceSubscribe(sender);
await this.client.sendPresenceUpdate((data?.options?.presence as WAPresence) ?? 'composing', sender);
await delay(remainingDelay);
await this.client.sendPresenceUpdate('paused', sender);
}
} else {
await this.client.presenceSubscribe(sender);
await this.client.sendPresenceUpdate((data?.options?.presence as WAPresence) ?? 'composing', sender);
await delay(data?.options?.delay);
await this.client.sendPresenceUpdate('paused', sender);
}
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
throw new BadRequestException(error.toString()); throw new BadRequestException(error.toString());
@@ -2174,15 +2331,6 @@ export class BaileysStartupService extends WAStartupService {
mimetype = response.headers['content-type']; mimetype = response.headers['content-type'];
} }
// if (isURL(mediaMessage.media)) {
// const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' });
// mimetype = response.headers['content-type'];
// console.log('mediaMessage.mimetype2', mimetype);
// } else {
// mimetype = getMIMEType(mediaMessage.fileName);
// console.log('mediaMessage.mimetype3', mimetype);
// }
} }
this.logger.verbose('Mimetype: ' + mimetype); this.logger.verbose('Mimetype: ' + mimetype);
@@ -2309,7 +2457,7 @@ export class BaileysStartupService extends WAStartupService {
if (isURL(audio)) { if (isURL(audio)) {
this.logger.verbose('Audio is url'); this.logger.verbose('Audio is url');
outputAudio = `${join(this.storePath, 'temp', `${hash}.ogg`)}`; outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`;
tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`;
this.logger.verbose('Output audio path: ' + outputAudio); this.logger.verbose('Output audio path: ' + outputAudio);
@@ -2320,25 +2468,14 @@ export class BaileysStartupService extends WAStartupService {
this.logger.verbose('Including timestamp in url: ' + url); this.logger.verbose('Including timestamp in url: ' + url);
let config: any = { const response = await axios.get(url, { responseType: 'arraybuffer' });
responseType: 'arraybuffer',
};
if (this.localProxy.enabled) {
config = {
...config,
httpsAgent: makeProxyAgent(this.localProxy.proxy),
};
}
const response = await axios.get(url, config);
this.logger.verbose('Getting audio from url'); this.logger.verbose('Getting audio from url');
fs.writeFileSync(tempAudioPath, response.data); fs.writeFileSync(tempAudioPath, response.data);
} else { } else {
this.logger.verbose('Audio is base64'); this.logger.verbose('Audio is base64');
outputAudio = `${join(this.storePath, 'temp', `${hash}.ogg`)}`; outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`;
tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`;
this.logger.verbose('Output audio path: ' + outputAudio); this.logger.verbose('Output audio path: ' + outputAudio);
@@ -2351,25 +2488,15 @@ export class BaileysStartupService extends WAStartupService {
this.logger.verbose('Converting audio to mp4'); this.logger.verbose('Converting audio to mp4');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// This fix was suggested by @PurpShell exec(`${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, (error) => {
ffmpeg.setFfmpegPath(ffmpegPath.path); fs.unlinkSync(tempAudioPath);
this.logger.verbose('Temp audio deleted');
ffmpeg() if (error) reject(error);
.input(tempAudioPath)
.outputFormat('ogg') this.logger.verbose('Audio converted to mp4');
.noVideo() resolve(outputAudio);
.audioCodec('libopus') });
.save(outputAudio)
.on('error', function (error) {
console.log('error', error);
fs.unlinkSync(tempAudioPath);
if (error) reject(error);
})
.on('end', async function () {
fs.unlinkSync(tempAudioPath);
resolve(outputAudio);
})
.run();
}); });
} }
@@ -2389,7 +2516,7 @@ export class BaileysStartupService extends WAStartupService {
{ {
audio: Buffer.from(audio, 'base64'), audio: Buffer.from(audio, 'base64'),
ptt: true, ptt: true,
mimetype: 'audio/ogg; codecs=opus', mimetype: 'audio/mp4',
}, },
{ presence: 'recording', delay: data?.options?.delay }, { presence: 'recording', delay: data?.options?.delay },
isChatwoot, isChatwoot,
@@ -2418,50 +2545,8 @@ export class BaileysStartupService extends WAStartupService {
); );
} }
public async buttonMessage(data: SendButtonDto) { public async buttonMessage() {
this.logger.verbose('Sending button message'); throw new BadRequestException('Method not available on WhatsApp Baileys');
const embeddedMedia: any = {};
let mediatype = 'TEXT';
if (data.buttonMessage?.mediaMessage) {
mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT';
embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message';
const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage);
embeddedMedia.message = generate.message[embeddedMedia.mediaKey];
embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`;
}
const btnItems = {
text: data.buttonMessage.buttons.map((btn) => btn.buttonText),
ids: data.buttonMessage.buttons.map((btn) => btn.buttonId),
};
if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) {
throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.');
}
return await this.sendMessageWithTyping(
data.number,
{
buttonsMessage: {
text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined,
contentText: embeddedMedia?.contentText ?? data.buttonMessage.description,
footerText: data.buttonMessage?.footerText,
buttons: data.buttonMessage.buttons.map((button) => {
return {
buttonText: {
displayText: button.buttonText,
},
buttonId: button.buttonId,
type: 1,
};
}),
headerType: proto.Message.ButtonsMessage.HeaderType[mediatype],
[embeddedMedia?.mediaKey]: embeddedMedia?.message,
},
},
data?.options,
);
} }
public async locationMessage(data: SendLocationDto) { public async locationMessage(data: SendLocationDto) {
@@ -2754,6 +2839,45 @@ export class BaileysStartupService extends WAStartupService {
} }
} }
public async markChatUnread(data: MarkChatUnreadDto) {
this.logger.verbose('Marking chat as unread');
try {
let last_message = data.lastMessage;
let number = data.chat;
if (!last_message && number) {
last_message = await this.getLastMessage(number);
} else {
last_message = data.lastMessage;
last_message.messageTimestamp = last_message?.messageTimestamp ?? Date.now();
number = last_message?.key?.remoteJid;
}
if (!last_message || Object.keys(last_message).length === 0) {
throw new NotFoundException('Last message not found');
}
await this.client.chatModify(
{
markRead: false,
lastMessages: [last_message],
},
this.createJid(number),
);
return {
chatId: number,
markedChatUnread: true,
};
} catch (error) {
throw new InternalServerErrorException({
markedChatUnread: false,
message: ['An error occurred while marked unread the chat. Open a calling.', error.toString()],
});
}
}
public async deleteMessage(del: DeleteMessage) { public async deleteMessage(del: DeleteMessage) {
this.logger.verbose('Deleting message'); this.logger.verbose('Deleting message');
try { try {
@@ -3102,6 +3226,38 @@ export class BaileysStartupService extends WAStartupService {
} }
// Group // Group
private async updateGroupMetadataCache(groupJid: string) {
try {
const meta = await this.client.groupMetadata(groupJid);
await groupMetadataCache.set(groupJid, {
timestamp: Date.now(),
data: meta,
});
return meta;
} catch (error) {
this.logger.error(error);
return null;
}
}
private async getGroupMetadataCache(groupJid: string) {
if (!isJidGroup(groupJid)) return null;
if (await groupMetadataCache.has(groupJid)) {
console.log('Has cache for group: ' + groupJid);
const meta = await groupMetadataCache.get(groupJid);
if (Date.now() - meta.timestamp > 3600000) {
await this.updateGroupMetadataCache(groupJid);
}
return meta.data;
}
return await this.updateGroupMetadataCache(groupJid);
}
public async createGroup(create: CreateGroupDto) { public async createGroup(create: CreateGroupDto) {
this.logger.verbose('Creating group: ' + create.subject); this.logger.verbose('Creating group: ' + create.subject);
try { try {

View File

@@ -7,7 +7,6 @@ import { getMIMEType } from 'node-mime-types';
import { ConfigService, Database, WaBusiness } from '../../../config/env.config'; import { ConfigService, Database, WaBusiness } from '../../../config/env.config';
import { BadRequestException, InternalServerErrorException } from '../../../exceptions'; import { BadRequestException, InternalServerErrorException } from '../../../exceptions';
import { RedisCache } from '../../../libs/redis.client';
import { NumberBusiness } from '../../dto/chat.dto'; import { NumberBusiness } from '../../dto/chat.dto';
import { import {
ContactMessage, ContactMessage,
@@ -24,18 +23,21 @@ import {
SendTextDto, SendTextDto,
} from '../../dto/sendMessage.dto'; } from '../../dto/sendMessage.dto';
import { ContactRaw, MessageRaw, MessageUpdateRaw, SettingsRaw } from '../../models'; import { ContactRaw, MessageRaw, MessageUpdateRaw, SettingsRaw } from '../../models';
import { ProviderFiles } from '../../provider/sessions';
import { RepositoryBroker } from '../../repository/repository.manager'; import { RepositoryBroker } from '../../repository/repository.manager';
import { Events, wa } from '../../types/wa.types'; import { Events, wa } from '../../types/wa.types';
import { CacheService } from './../cache.service'; import { CacheService } from './../cache.service';
import { WAStartupService } from './../whatsapp.service'; import { ChannelStartupService } from './../channel.service';
export class BusinessStartupService extends WAStartupService { export class BusinessStartupService extends ChannelStartupService {
constructor( constructor(
public readonly configService: ConfigService, public readonly configService: ConfigService,
public readonly eventEmitter: EventEmitter2, public readonly eventEmitter: EventEmitter2,
public readonly repository: RepositoryBroker, public readonly repository: RepositoryBroker,
public readonly cache: RedisCache, public readonly cache: CacheService,
public readonly chatwootCache: CacheService, public readonly chatwootCache: CacheService,
public readonly baileysCache: CacheService,
private readonly providerFiles: ProviderFiles,
) { ) {
super(configService, eventEmitter, repository, chatwootCache); super(configService, eventEmitter, repository, chatwootCache);
this.logger.verbose('BusinessStartupService initialized'); this.logger.verbose('BusinessStartupService initialized');
@@ -1258,6 +1260,9 @@ export class BusinessStartupService extends WAStartupService {
public async archiveChat() { public async archiveChat() {
throw new BadRequestException('Method not available on WhatsApp Business API'); throw new BadRequestException('Method not available on WhatsApp Business API');
} }
public async markChatUnread() {
throw new BadRequestException('Method not available on WhatsApp Business API');
}
public async fetchProfile() { public async fetchProfile() {
throw new BadRequestException('Method not available on WhatsApp Business API'); throw new BadRequestException('Method not available on WhatsApp Business API');
} }

View File

@@ -22,7 +22,8 @@ export class IntegrationService {
const result = await this.waMonitor.waInstances[instance.instanceName].findIntegration(); const result = await this.waMonitor.waInstances[instance.instanceName].findIntegration();
if (Object.keys(result).length === 0) { if (Object.keys(result).length === 0) {
throw new Error('Integration not found'); this.create(instance, { integration: 'WHATSAPP-BAILEYS', number: '', token: '' });
return { integration: 'WHATSAPP-BAILEYS', number: '', token: '' };
} }
return result; return result;

View File

@@ -5,11 +5,18 @@ import { Db } from 'mongodb';
import { Collection } from 'mongoose'; import { Collection } from 'mongoose';
import { join } from 'path'; import { join } from 'path';
import { Auth, ConfigService, Database, DelInstance, HttpServer, Redis } from '../../config/env.config'; import {
Auth,
CacheConf,
ConfigService,
Database,
DelInstance,
HttpServer,
ProviderSession,
} from '../../config/env.config';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { INSTANCE_DIR, STORE_DIR } from '../../config/path.config'; import { INSTANCE_DIR, STORE_DIR } from '../../config/path.config';
import { NotFoundException } from '../../exceptions'; import { NotFoundException } from '../../exceptions';
import { RedisCache } from '../../libs/redis.client';
import { import {
AuthModel, AuthModel,
ChamaaiModel, ChamaaiModel,
@@ -23,19 +30,22 @@ import {
WebhookModel, WebhookModel,
WebsocketModel, WebsocketModel,
} from '../models'; } from '../models';
import { ProviderFiles } from '../provider/sessions';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { Integration } from '../types/wa.types'; import { Integration } from '../types/wa.types';
import { CacheService } from './cache.service'; import { CacheService } from './cache.service';
import { BaileysStartupService } from './whatsapp/whatsapp.baileys.service'; import { BaileysStartupService } from './channels/whatsapp.baileys.service';
import { BusinessStartupService } from './whatsapp/whatsapp.business.service'; import { BusinessStartupService } from './channels/whatsapp.business.service';
export class WAMonitoringService { export class WAMonitoringService {
constructor( constructor(
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly repository: RepositoryBroker, private readonly repository: RepositoryBroker,
private readonly cache: RedisCache, private readonly cache: CacheService,
private readonly chatwootCache: CacheService, private readonly chatwootCache: CacheService,
private readonly baileysCache: CacheService,
private readonly providerFiles: ProviderFiles,
) { ) {
this.logger.verbose('instance created'); this.logger.verbose('instance created');
@@ -43,7 +53,7 @@ export class WAMonitoringService {
this.noConnection(); this.noConnection();
Object.assign(this.db, configService.get<Database>('DATABASE')); Object.assign(this.db, configService.get<Database>('DATABASE'));
Object.assign(this.redis, configService.get<Redis>('REDIS')); Object.assign(this.redis, configService.get<CacheConf>('CACHE'));
this.dbInstance = this.db.ENABLED this.dbInstance = this.db.ENABLED
? this.repository.dbServer?.db(this.db.CONNECTION.DB_PREFIX_NAME + '-instances') ? this.repository.dbServer?.db(this.db.CONNECTION.DB_PREFIX_NAME + '-instances')
@@ -51,13 +61,15 @@ export class WAMonitoringService {
} }
private readonly db: Partial<Database> = {}; private readonly db: Partial<Database> = {};
private readonly redis: Partial<Redis> = {}; private readonly redis: Partial<CacheConf> = {};
private dbInstance: Db; private dbInstance: Db;
private readonly logger = new Logger(WAMonitoringService.name); private readonly logger = new Logger(WAMonitoringService.name);
public readonly waInstances: Record<string, BaileysStartupService | BusinessStartupService> = {}; public readonly waInstances: Record<string, BaileysStartupService | BusinessStartupService> = {};
private readonly providerSession = Object.freeze(this.configService.get<ProviderSession>('PROVIDER'));
public delInstanceTime(instance: string) { public delInstanceTime(instance: string) {
const time = this.configService.get<DelInstance>('DEL_INSTANCE'); const time = this.configService.get<DelInstance>('DEL_INSTANCE');
if (typeof time === 'number' && time > 0) { if (typeof time === 'number' && time > 0) {
@@ -83,7 +95,7 @@ export class WAMonitoringService {
} }
} }
public async instanceInfo(instanceName?: string) { public async instanceInfo(instanceName?: string, arrayReturn = false) {
this.logger.verbose('get instance info'); this.logger.verbose('get instance info');
if (instanceName && !this.waInstances[instanceName]) { if (instanceName && !this.waInstances[instanceName]) {
throw new NotFoundException(`Instance "${instanceName}" not found`); throw new NotFoundException(`Instance "${instanceName}" not found`);
@@ -171,6 +183,9 @@ export class WAMonitoringService {
this.logger.verbose('return instance info: ' + instances.length); this.logger.verbose('return instance info: ' + instances.length);
if (arrayReturn) {
return [instances.find((i) => i.instance.instanceName === instanceName) ?? instances];
}
return instances.find((i) => i.instance.instanceName === instanceName) ?? instances; return instances.find((i) => i.instance.instanceName === instanceName) ?? instances;
} }
@@ -212,7 +227,7 @@ export class WAMonitoringService {
}); });
this.logger.verbose('instance files deleted: ' + name); this.logger.verbose('instance files deleted: ' + name);
}); });
} else if (!this.redis.ENABLED) { } else if (!this.redis.REDIS.ENABLED && !this.redis.REDIS.SAVE_INSTANCES) {
const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' });
for await (const dirent of dir) { for await (const dirent of dir) {
if (dirent.isDirectory()) { if (dirent.isDirectory()) {
@@ -247,20 +262,25 @@ export class WAMonitoringService {
return; return;
} }
if (this.redis.ENABLED) { if (this.redis.REDIS.ENABLED && this.redis.REDIS.SAVE_INSTANCES) {
this.logger.verbose('cleaning up instance in redis: ' + instanceName); this.logger.verbose('cleaning up instance in redis: ' + instanceName);
this.cache.reference = instanceName; await this.cache.delete(instanceName);
await this.cache.delAll();
return; return;
} }
this.logger.verbose('cleaning up instance in files: ' + instanceName); this.logger.verbose('cleaning up instance in files: ' + instanceName);
if (this.providerSession?.ENABLED) {
await this.providerFiles.removeSession(instanceName);
}
rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true });
} }
public async cleaningStoreFiles(instanceName: string) { public async cleaningStoreFiles(instanceName: string) {
if (!this.db.ENABLED) { if (!this.db.ENABLED) {
this.logger.verbose('cleaning store files instance: ' + instanceName); this.logger.verbose('cleaning store files instance: ' + instanceName);
if (this.providerSession?.ENABLED) {
await this.providerFiles.removeSession(instanceName);
}
rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true });
execSync(`rm -rf ${join(STORE_DIR, 'chats', instanceName)}`); execSync(`rm -rf ${join(STORE_DIR, 'chats', instanceName)}`);
@@ -303,7 +323,9 @@ export class WAMonitoringService {
this.logger.verbose('Loading instances'); this.logger.verbose('Loading instances');
try { try {
if (this.redis.ENABLED) { if (this.providerSession?.ENABLED) {
await this.loadInstancesFromProvider();
} else if (this.redis.REDIS.ENABLED && this.redis.REDIS.SAVE_INSTANCES) {
await this.loadInstancesFromRedis(); await this.loadInstancesFromRedis();
} else if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { } else if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) {
await this.loadInstancesFromDatabase(); await this.loadInstancesFromDatabase();
@@ -346,6 +368,8 @@ export class WAMonitoringService {
this.repository, this.repository,
this.cache, this.cache,
this.chatwootCache, this.chatwootCache,
this.baileysCache,
this.providerFiles,
); );
instance.instanceName = name; instance.instanceName = name;
@@ -356,6 +380,8 @@ export class WAMonitoringService {
this.repository, this.repository,
this.cache, this.cache,
this.chatwootCache, this.chatwootCache,
this.baileysCache,
this.providerFiles,
); );
instance.instanceName = name; instance.instanceName = name;
@@ -374,12 +400,11 @@ export class WAMonitoringService {
private async loadInstancesFromRedis() { private async loadInstancesFromRedis() {
this.logger.verbose('Redis enabled'); this.logger.verbose('Redis enabled');
await this.cache.connect(this.redis as Redis); const keys = await this.cache.keys();
const keys = await this.cache.getInstanceKeys();
if (keys?.length > 0) { if (keys?.length > 0) {
this.logger.verbose('Reading instance keys and setting instances'); this.logger.verbose('Reading instance keys and setting instances');
await Promise.all(keys.map((k) => this.setInstance(k.split(':')[1]))); await Promise.all(keys.map((k) => this.setInstance(k.split(':')[2])));
} else { } else {
this.logger.verbose('No instance keys found'); this.logger.verbose('No instance keys found');
} }
@@ -398,6 +423,18 @@ export class WAMonitoringService {
} }
} }
private async loadInstancesFromProvider() {
this.logger.verbose('Provider in files enabled');
const [instances] = await this.providerFiles.allInstances();
if (!instances?.data) {
this.logger.verbose('No instances found');
return;
}
await Promise.all(instances?.data?.map(async (instanceName: string) => this.setInstance(instanceName)));
}
private async loadInstancesFromFiles() { private async loadInstancesFromFiles() {
this.logger.verbose('Store in files enabled'); this.logger.verbose('Store in files enabled');
const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' });

View File

@@ -69,6 +69,7 @@ export declare namespace wa {
number?: string; number?: string;
reopen_conversation?: boolean; reopen_conversation?: boolean;
conversation_pending?: boolean; conversation_pending?: boolean;
merge_brazil_contacts?: boolean;
import_contacts?: boolean; import_contacts?: boolean;
import_messages?: boolean; import_messages?: boolean;
days_limit_import_messages?: number; days_limit_import_messages?: number;

View File

@@ -1,8 +1,11 @@
import { CacheConf, ConfigService } from '../../../../config/env.config'; import { ICache } from '../api/abstract/abstract.cache';
import { ICache } from '../../../abstract/abstract.cache'; import { CacheConf, ConfigService } from '../config/env.config';
import { Logger } from '../config/logger.config';
import { LocalCache } from './localcache'; import { LocalCache } from './localcache';
import { RedisCache } from './rediscache'; import { RedisCache } from './rediscache';
const logger = new Logger('Redis');
export class CacheEngine { export class CacheEngine {
private engine: ICache; private engine: ICache;
@@ -14,6 +17,8 @@ export class CacheEngine {
} else if (cacheConf?.LOCAL?.ENABLED) { } else if (cacheConf?.LOCAL?.ENABLED) {
this.engine = new LocalCache(configService, module); this.engine = new LocalCache(configService, module);
} }
logger.info(`RedisCache initialized for ${module}`);
} }
public getEngine() { public getEngine() {

View File

@@ -1,7 +1,7 @@
import NodeCache from 'node-cache'; import NodeCache from 'node-cache';
import { CacheConf, CacheConfLocal, ConfigService } from '../../../../config/env.config'; import { ICache } from '../api/abstract/abstract.cache';
import { ICache } from '../../../abstract/abstract.cache'; import { CacheConf, CacheConfLocal, ConfigService } from '../config/env.config';
export class LocalCache implements ICache { export class LocalCache implements ICache {
private conf: CacheConfLocal; private conf: CacheConfLocal;
@@ -45,4 +45,17 @@ export class LocalCache implements ICache {
buildKey(key: string) { buildKey(key: string) {
return `${this.module}:${key}`; return `${this.module}:${key}`;
} }
async hGet() {
console.log('hGet not implemented');
}
async hSet() {
console.log('hSet not implemented');
}
async hDelete() {
console.log('hDelete not implemented');
return 0;
}
} }

View File

@@ -1,7 +1,7 @@
import { createClient, RedisClientType } from 'redis'; import { createClient, RedisClientType } from 'redis';
import { CacheConf, CacheConfRedis, configService } from '../../../../config/env.config'; import { CacheConf, CacheConfRedis, configService } from '../config/env.config';
import { Logger } from '../../../../config/logger.config'; import { Logger } from '../config/logger.config';
class Redis { class Redis {
private logger = new Logger(Redis.name); private logger = new Logger(Redis.name);

View File

@@ -1,8 +1,9 @@
import { BufferJSON } from '@whiskeysockets/baileys';
import { RedisClientType } from 'redis'; import { RedisClientType } from 'redis';
import { CacheConf, CacheConfRedis, ConfigService } from '../../../../config/env.config'; import { ICache } from '../api/abstract/abstract.cache';
import { Logger } from '../../../../config/logger.config'; import { CacheConf, CacheConfRedis, ConfigService } from '../config/env.config';
import { ICache } from '../../../abstract/abstract.cache'; import { Logger } from '../config/logger.config';
import { redisClient } from './rediscache.client'; import { redisClient } from './rediscache.client';
export class RedisCache implements ICache { export class RedisCache implements ICache {
@@ -14,7 +15,6 @@ export class RedisCache implements ICache {
this.conf = this.configService.get<CacheConf>('CACHE')?.REDIS; this.conf = this.configService.get<CacheConf>('CACHE')?.REDIS;
this.client = redisClient.getConnection(); this.client = redisClient.getConnection();
} }
async get(key: string): Promise<any> { async get(key: string): Promise<any> {
try { try {
return JSON.parse(await this.client.get(this.buildKey(key))); return JSON.parse(await this.client.get(this.buildKey(key)));
@@ -23,6 +23,20 @@ export class RedisCache implements ICache {
} }
} }
async hGet(key: string, field: string) {
try {
const data = await this.client.hGet(this.buildKey(key), field);
if (data) {
return JSON.parse(data, BufferJSON.reviver);
}
return null;
} catch (error) {
this.logger.error(error);
}
}
async set(key: string, value: any, ttl?: number) { async set(key: string, value: any, ttl?: number) {
try { try {
await this.client.setEx(this.buildKey(key), ttl || this.conf?.TTL, JSON.stringify(value)); await this.client.setEx(this.buildKey(key), ttl || this.conf?.TTL, JSON.stringify(value));
@@ -31,6 +45,16 @@ export class RedisCache implements ICache {
} }
} }
async hSet(key: string, field: string, value: any) {
try {
const json = JSON.stringify(value, BufferJSON.replacer);
await this.client.hSet(this.buildKey(key), field, json);
} catch (error) {
this.logger.error(error);
}
}
async has(key: string) { async has(key: string) {
try { try {
return (await this.client.exists(this.buildKey(key))) > 0; return (await this.client.exists(this.buildKey(key))) > 0;
@@ -47,6 +71,14 @@ export class RedisCache implements ICache {
} }
} }
async hDelete(key: string, field: string) {
try {
return await this.client.hDel(this.buildKey(key), field);
} catch (error) {
this.logger.error(error);
}
}
async deleteAll(appendCriteria?: string) { async deleteAll(appendCriteria?: string) {
try { try {
const keys = await this.keys(appendCriteria); const keys = await this.keys(appendCriteria);

View File

@@ -28,6 +28,13 @@ export type Log = {
BAILEYS: LogBaileys; BAILEYS: LogBaileys;
}; };
export type ProviderSession = {
ENABLED: boolean;
HOST: string;
PORT: string;
PREFIX: string;
};
export type SaveData = { export type SaveData = {
INSTANCE: boolean; INSTANCE: boolean;
NEW_MESSAGE: boolean; NEW_MESSAGE: boolean;
@@ -63,17 +70,42 @@ export type Database = {
SAVE_DATA: SaveData; SAVE_DATA: SaveData;
}; };
export type Redis = { export type EventsRabbitmq = {
ENABLED: boolean; APPLICATION_STARTUP: boolean;
URI: string; INSTANCE_CREATE: boolean;
PREFIX_KEY: string; INSTANCE_DELETE: boolean;
QRCODE_UPDATED: boolean;
MESSAGES_SET: boolean;
MESSAGES_UPSERT: boolean;
MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean;
CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean;
PRESENCE_UPDATE: boolean;
CHATS_SET: boolean;
CHATS_UPDATE: boolean;
CHATS_DELETE: boolean;
CHATS_UPSERT: boolean;
CONNECTION_UPDATE: boolean;
LABELS_EDIT: boolean;
LABELS_ASSOCIATION: boolean;
GROUPS_UPSERT: boolean;
GROUP_UPDATE: boolean;
GROUP_PARTICIPANTS_UPDATE: boolean;
CALL: boolean;
NEW_JWT_TOKEN: boolean;
TYPEBOT_START: boolean;
TYPEBOT_CHANGE_STATUS: boolean;
}; };
export type Rabbitmq = { export type Rabbitmq = {
ENABLED: boolean; ENABLED: boolean;
MODE: string; // global, single, isolated
EXCHANGE_NAME: string; // available for global and single, isolated mode will use instance name as exchange
URI: string; URI: string;
EXCHANGE_NAME: string;
GLOBAL_ENABLED: boolean;
EVENTS: EventsRabbitmq;
}; };
export type Sqs = { export type Sqs = {
@@ -153,6 +185,7 @@ export type CacheConfRedis = {
URI: string; URI: string;
PREFIX_KEY: string; PREFIX_KEY: string;
TTL: number; TTL: number;
SAVE_INSTANCES: boolean;
}; };
export type CacheConfLocal = { export type CacheConfLocal = {
ENABLED: boolean; ENABLED: boolean;
@@ -160,7 +193,7 @@ export type CacheConfLocal = {
}; };
export type SslConf = { PRIVKEY: string; FULLCHAIN: string }; export type SslConf = { PRIVKEY: string; FULLCHAIN: string };
export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook }; export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook };
export type ConfigSessionPhone = { CLIENT: string; NAME: string }; export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string };
export type QrCode = { LIMIT: number; COLOR: string }; export type QrCode = { LIMIT: number; COLOR: string };
export type Typebot = { API_VERSION: string; KEEP_OPEN: boolean }; export type Typebot = { API_VERSION: string; KEEP_OPEN: boolean };
export type Chatwoot = { export type Chatwoot = {
@@ -183,10 +216,10 @@ export interface Env {
SERVER: HttpServer; SERVER: HttpServer;
CORS: Cors; CORS: Cors;
SSL_CONF: SslConf; SSL_CONF: SslConf;
PROVIDER: ProviderSession;
STORE: StoreConf; STORE: StoreConf;
CLEAN_STORE: CleanStoreConf; CLEAN_STORE: CleanStoreConf;
DATABASE: Database; DATABASE: Database;
REDIS: Redis;
RABBITMQ: Rabbitmq; RABBITMQ: Rabbitmq;
SQS: Sqs; SQS: Sqs;
WEBSOCKET: Websocket; WEBSOCKET: Websocket;
@@ -249,6 +282,12 @@ export class ConfigService {
PRIVKEY: process.env?.SSL_CONF_PRIVKEY || '', PRIVKEY: process.env?.SSL_CONF_PRIVKEY || '',
FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN || '', FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN || '',
}, },
PROVIDER: {
ENABLED: process.env?.PROVIDER_ENABLED === 'true',
HOST: process.env.PROVIDER_HOST,
PORT: process.env?.PROVIDER_PORT || '5656',
PREFIX: process.env?.PROVIDER_PREFIX || 'evolution',
},
STORE: { STORE: {
MESSAGES: process.env?.STORE_MESSAGES === 'true', MESSAGES: process.env?.STORE_MESSAGES === 'true',
MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true', MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true',
@@ -257,8 +296,8 @@ export class ConfigService {
LABELS: process.env?.STORE_LABELS === 'true', LABELS: process.env?.STORE_LABELS === 'true',
}, },
CLEAN_STORE: { CLEAN_STORE: {
CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_INTERVAL)
? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL) ? Number.parseInt(process.env.CLEAN_STORE_CLEANING_INTERVAL)
: 7200, : 7200,
MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true', MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true',
MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true', MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true',
@@ -280,16 +319,40 @@ export class ConfigService {
LABELS: process.env?.DATABASE_SAVE_DATA_LABELS === 'true', LABELS: process.env?.DATABASE_SAVE_DATA_LABELS === 'true',
}, },
}, },
REDIS: {
ENABLED: process.env?.REDIS_ENABLED === 'true',
URI: process.env.REDIS_URI || '',
PREFIX_KEY: process.env.REDIS_PREFIX_KEY || 'evolution',
},
RABBITMQ: { RABBITMQ: {
ENABLED: process.env?.RABBITMQ_ENABLED === 'true', ENABLED: process.env?.RABBITMQ_ENABLED === 'true',
MODE: process.env?.RABBITMQ_MODE || 'isolated', GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true',
EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange', EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange',
URI: process.env.RABBITMQ_URI || '', URI: process.env.RABBITMQ_URI || '',
EVENTS: {
APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true',
INSTANCE_CREATE: process.env?.RABBITMQ_EVENTS_INSTANCE_CREATE === 'true',
INSTANCE_DELETE: process.env?.RABBITMQ_EVENTS_INSTANCE_DELETE === 'true',
QRCODE_UPDATED: process.env?.RABBITMQ_EVENTS_QRCODE_UPDATED === 'true',
MESSAGES_SET: process.env?.RABBITMQ_EVENTS_MESSAGES_SET === 'true',
MESSAGES_UPSERT: process.env?.RABBITMQ_EVENTS_MESSAGES_UPSERT === 'true',
MESSAGES_UPDATE: process.env?.RABBITMQ_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.RABBITMQ_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE === 'true',
CONTACTS_SET: process.env?.RABBITMQ_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.RABBITMQ_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.RABBITMQ_EVENTS_CONTACTS_UPSERT === 'true',
PRESENCE_UPDATE: process.env?.RABBITMQ_EVENTS_PRESENCE_UPDATE === 'true',
CHATS_SET: process.env?.RABBITMQ_EVENTS_CHATS_SET === 'true',
CHATS_UPDATE: process.env?.RABBITMQ_EVENTS_CHATS_UPDATE === 'true',
CHATS_UPSERT: process.env?.RABBITMQ_EVENTS_CHATS_UPSERT === 'true',
CHATS_DELETE: process.env?.RABBITMQ_EVENTS_CHATS_DELETE === 'true',
CONNECTION_UPDATE: process.env?.RABBITMQ_EVENTS_CONNECTION_UPDATE === 'true',
LABELS_EDIT: process.env?.RABBITMQ_EVENTS_LABELS_EDIT === 'true',
LABELS_ASSOCIATION: process.env?.RABBITMQ_EVENTS_LABELS_ASSOCIATION === 'true',
GROUPS_UPSERT: process.env?.RABBITMQ_EVENTS_GROUPS_UPSERT === 'true',
GROUP_UPDATE: process.env?.RABBITMQ_EVENTS_GROUPS_UPDATE === 'true',
GROUP_PARTICIPANTS_UPDATE: process.env?.RABBITMQ_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true',
CALL: process.env?.RABBITMQ_EVENTS_CALL === 'true',
NEW_JWT_TOKEN: process.env?.RABBITMQ_EVENTS_NEW_JWT_TOKEN === 'true',
TYPEBOT_START: process.env?.RABBITMQ_EVENTS_TYPEBOT_START === 'true',
TYPEBOT_CHANGE_STATUS: process.env?.RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
},
}, },
SQS: { SQS: {
ENABLED: process.env?.SQS_ENABLED === 'true', ENABLED: process.env?.SQS_ENABLED === 'true',
@@ -371,6 +434,7 @@ export class ConfigService {
CONFIG_SESSION_PHONE: { CONFIG_SESSION_PHONE: {
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'Chrome', NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'Chrome',
VERSION: process.env?.CONFIG_SESSION_PHONE_VERSION || null,
}, },
QRCODE: { QRCODE: {
LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30,
@@ -398,6 +462,7 @@ export class ConfigService {
URI: process.env?.CACHE_REDIS_URI || '', URI: process.env?.CACHE_REDIS_URI || '',
PREFIX_KEY: process.env?.CACHE_REDIS_PREFIX_KEY || 'evolution-cache', PREFIX_KEY: process.env?.CACHE_REDIS_PREFIX_KEY || 'evolution-cache',
TTL: Number.parseInt(process.env?.CACHE_REDIS_TTL) || 604800, TTL: Number.parseInt(process.env?.CACHE_REDIS_TTL) || 604800,
SAVE_INSTANCES: process.env?.CACHE_REDIS_SAVE_INSTANCES === 'true',
}, },
LOCAL: { LOCAL: {
ENABLED: process.env?.CACHE_LOCAL_ENABLED === 'true', ENABLED: process.env?.CACHE_LOCAL_ENABLED === 'true',

View File

@@ -60,10 +60,16 @@ export class Logger {
private readonly configService = configService; private readonly configService = configService;
constructor(private context = 'Logger') {} constructor(private context = 'Logger') {}
private instance = null;
public setContext(value: string) { public setContext(value: string) {
this.context = value; this.context = value;
} }
public setInstance(value: string) {
this.instance = value;
}
private console(value: any, type: Type) { private console(value: any, type: Type) {
const types: Type[] = []; const types: Type[] = [];
@@ -76,6 +82,8 @@ export class Logger {
/*Command.UNDERSCORE +*/ Command.BRIGHT + Level[type], /*Command.UNDERSCORE +*/ Command.BRIGHT + Level[type],
'[Evolution API]', '[Evolution API]',
Command.BRIGHT + Color[type], Command.BRIGHT + Color[type],
this.instance ? `[${this.instance}]` : '',
Command.BRIGHT + Color[type],
`v${packageJson.version}`, `v${packageJson.version}`,
Command.BRIGHT + Color[type], Command.BRIGHT + Color[type],
process.pid.toString(), process.pid.toString(),
@@ -99,6 +107,7 @@ export class Logger {
} else { } else {
console.log( console.log(
'[Evolution API]', '[Evolution API]',
this.instance ? `[${this.instance}]` : '',
process.pid.toString(), process.pid.toString(),
'-', '-',
`${formatDateLog(Date.now())} `, `${formatDateLog(Date.now())} `,

View File

@@ -49,6 +49,14 @@ LOG:
DEL_INSTANCE: false # or false DEL_INSTANCE: false # or false
DEL_TEMP_INSTANCES: true # Delete instances with status closed on start DEL_TEMP_INSTANCES: true # Delete instances with status closed on start
# Seesion Files Providers
# Provider responsible for managing credentials files and WhatsApp sessions.
PROVIDER:
ENABLED: true
HOST: 127.0.0.1
PORT: 5656
PREFIX: evolution
# Temporary data storage # Temporary data storage
STORE: STORE:
MESSAGES: true MESSAGES: true
@@ -77,16 +85,37 @@ DATABASE:
CONTACTS: false CONTACTS: false
CHATS: false CHATS: false
REDIS:
ENABLED: false
URI: "redis://localhost:6379"
PREFIX_KEY: "evolution"
RABBITMQ: RABBITMQ:
ENABLED: false ENABLED: false
MODE: "global"
EXCHANGE_NAME: "evolution_exchange"
URI: "amqp://guest:guest@localhost:5672" URI: "amqp://guest:guest@localhost:5672"
EXCHANGE_NAME: evolution_exchange
GLOBAL_ENABLED: true
EVENTS:
APPLICATION_STARTUP: false
INSTANCE_CREATE: false
INSTANCE_DELETE: false
QRCODE_UPDATED: false
MESSAGES_SET: false
MESSAGES_UPSERT: true
MESSAGES_UPDATE: true
MESSAGES_DELETE: false
SEND_MESSAGE: false
CONTACTS_SET: false
CONTACTS_UPSERT: false
CONTACTS_UPDATE: false
PRESENCE_UPDATE: false
CHATS_SET: false
CHATS_UPSERT: false
CHATS_UPDATE: false
CHATS_DELETE: false
GROUPS_UPSERT: true
GROUP_UPDATE: true
GROUP_PARTICIPANTS_UPDATE: true
CONNECTION_UPDATE: true
CALL: false
# This events is used with Typebot
TYPEBOT_START: false
TYPEBOT_CHANGE_STATUS: false
SQS: SQS:
ENABLED: true ENABLED: true
@@ -173,7 +202,7 @@ CHATWOOT:
# This db connection is used to import messages from whatsapp to chatwoot database # This db connection is used to import messages from whatsapp to chatwoot database
DATABASE: DATABASE:
CONNECTION: CONNECTION:
URI: "postgres://user:password@hostname:port/dbname" URI: "postgres://user:password@hostname:port/dbname?sslmode=disable"
PLACEHOLDER_MEDIA_MESSAGE: true PLACEHOLDER_MEDIA_MESSAGE: true
# Cache to optimize application performance # Cache to optimize application performance
@@ -181,8 +210,9 @@ CACHE:
REDIS: REDIS:
ENABLED: false ENABLED: false
URI: "redis://localhost:6379" URI: "redis://localhost:6379"
PREFIX_KEY: "evolution-cache" PREFIX_KEY: "evolution"
TTL: 604800 TTL: 604800
SAVE_INSTANCES: false
LOCAL: LOCAL:
ENABLED: false ENABLED: false
TTL: 86400 TTL: 86400

View File

@@ -25,7 +25,7 @@ info:
</font> </font>
[![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/26869335-5546d063-156b-4529-915f-909dd628c090?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D26869335-5546d063-156b-4529-915f-909dd628c090%26entityType%3Dcollection%26workspaceId%3D339a4ee7-378b-45c9-b5b8-fd2c0a9c2442) [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/26869335-5546d063-156b-4529-915f-909dd628c090?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D26869335-5546d063-156b-4529-915f-909dd628c090%26entityType%3Dcollection%26workspaceId%3D339a4ee7-378b-45c9-b5b8-fd2c0a9c2442)
version: 1.7.2 version: 1.8.0
contact: contact:
name: DavidsonGomes name: DavidsonGomes
email: contato@agenciadgcode.com email: contato@agenciadgcode.com
@@ -194,6 +194,8 @@ paths:
type: string type: string
description: Check the connection state of your instance. description: Check the connection state of your instance.
example: "evolution" example: "evolution"
security:
- apikeyAuth: []
responses: responses:
"200": "200":
description: Successful response description: Successful response
@@ -2242,6 +2244,9 @@ paths:
conversation_pending: conversation_pending:
type: boolean type: boolean
description: "Indicates whether to mark conversations as pending." description: "Indicates whether to mark conversations as pending."
merge_brazil_contacts:
type: boolean
description: "Indicates whether to merge Brazil numbers in case of numbers with and without ninth digit."
import_contacts: import_contacts:
type: boolean type: boolean
description: "Indicates whether to import contacts from phone to Chatwoot when connecting." description: "Indicates whether to import contacts from phone to Chatwoot when connecting."

View File

@@ -1,123 +0,0 @@
import { createClient, RedisClientType } from '@redis/client';
import { BufferJSON } from '@whiskeysockets/baileys';
import { Redis } from '../config/env.config';
import { Logger } from '../config/logger.config';
export class RedisCache {
private readonly logger = new Logger(RedisCache.name);
private client: RedisClientType;
private statusConnection = false;
private instanceName: string;
private redisEnv: Redis;
constructor() {
this.logger.verbose('RedisCache instance created');
process.on('beforeExit', () => {
this.logger.verbose('RedisCache instance destroyed');
this.disconnect();
});
}
public set reference(reference: string) {
this.logger.verbose('set reference: ' + reference);
this.instanceName = reference;
}
public async connect(redisEnv: Redis) {
this.logger.verbose('Connecting to Redis...');
this.client = createClient({ url: redisEnv.URI });
this.client.on('error', (err) => this.logger.error('Redis Client Error ' + err));
await this.client.connect();
this.statusConnection = true;
this.redisEnv = redisEnv;
this.logger.verbose(`Connected to ${redisEnv.URI}`);
}
public async disconnect() {
if (this.statusConnection) {
await this.client.disconnect();
this.statusConnection = false;
this.logger.verbose('Redis client disconnected');
}
}
public async getInstanceKeys(): Promise<string[]> {
const keys: string[] = [];
try {
this.logger.verbose('Fetching instance keys');
for await (const key of this.client.scanIterator({ MATCH: `${this.redisEnv.PREFIX_KEY}:*` })) {
keys.push(key);
}
return keys;
} catch (error) {
this.logger.error('Error fetching instance keys ' + error);
throw error;
}
}
public async keyExists(key?: string) {
try {
const keys = await this.getInstanceKeys();
const targetKey = key || this.instanceName;
this.logger.verbose('keyExists: ' + targetKey);
return keys.includes(targetKey);
} catch (error) {
return false;
}
}
public async setData(field: string, data: any) {
try {
this.logger.verbose('setData: ' + field);
const json = JSON.stringify(data, BufferJSON.replacer);
await this.client.hSet(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field, json);
return true;
} catch (error) {
this.logger.error(error);
return false;
}
}
public async getData(field: string): Promise<any | null> {
try {
this.logger.verbose('getData: ' + field);
const data = await this.client.hGet(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field);
if (data) {
this.logger.verbose('getData: ' + field + ' success');
return JSON.parse(data, BufferJSON.reviver);
}
this.logger.verbose('getData: ' + field + ' not found');
return null;
} catch (error) {
this.logger.error(error);
return null;
}
}
public async removeData(field: string): Promise<boolean> {
try {
this.logger.verbose('removeData: ' + field);
await this.client.hDel(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field);
return true;
} catch (error) {
this.logger.error(error);
return false;
}
}
public async delAll(hash?: string): Promise<boolean> {
try {
const targetHash = hash || this.redisEnv.PREFIX_KEY + ':' + this.instanceName;
this.logger.verbose('instance delAll: ' + targetHash);
const result = await this.client.del(targetHash);
return !!result;
} catch (error) {
this.logger.error(error);
return false;
}
}
}

View File

@@ -6,12 +6,13 @@ import cors from 'cors';
import express, { json, NextFunction, Request, Response, urlencoded } from 'express'; import express, { json, NextFunction, Request, Response, urlencoded } from 'express';
import { join } from 'path'; import { join } from 'path';
import { initAMQP } from './api/integrations/rabbitmq/libs/amqp.server'; import { initAMQP, initGlobalQueues } from './api/integrations/rabbitmq/libs/amqp.server';
import { initSQS } from './api/integrations/sqs/libs/sqs.server'; import { initSQS } from './api/integrations/sqs/libs/sqs.server';
import { initIO } from './api/integrations/websocket/libs/socket.server'; import { initIO } from './api/integrations/websocket/libs/socket.server';
import { ProviderFiles } from './api/provider/sessions';
import { HttpStatus, router } from './api/routes/index.router'; import { HttpStatus, router } from './api/routes/index.router';
import { waMonitor } from './api/server.module'; import { waMonitor } from './api/server.module';
import { Auth, configService, Cors, HttpServer, Rabbitmq, Sqs, Webhook } from './config/env.config'; import { Auth, configService, Cors, HttpServer, ProviderSession, Rabbitmq, Sqs, Webhook } from './config/env.config';
import { onUnexpectedError } from './config/error.config'; import { onUnexpectedError } from './config/error.config';
import { Logger } from './config/logger.config'; import { Logger } from './config/logger.config';
import { ROOT_DIR } from './config/path.config'; import { ROOT_DIR } from './config/path.config';
@@ -22,10 +23,18 @@ function initWA() {
waMonitor.loadInstance(); waMonitor.loadInstance();
} }
function bootstrap() { async function bootstrap() {
const logger = new Logger('SERVER'); const logger = new Logger('SERVER');
const app = express(); const app = express();
let providerFiles: ProviderFiles = null;
if (configService.get<ProviderSession>('PROVIDER')?.ENABLED) {
providerFiles = new ProviderFiles(configService);
await providerFiles.onModuleInit();
logger.info('Provider:Files - ON');
}
app.use( app.use(
cors({ cors({
origin(requestOrigin, callback) { origin(requestOrigin, callback) {
@@ -128,7 +137,11 @@ function bootstrap() {
initIO(server); initIO(server);
if (configService.get<Rabbitmq>('RABBITMQ')?.ENABLED) initAMQP(); if (configService.get<Rabbitmq>('RABBITMQ')?.ENABLED) {
initAMQP().then(() => {
if (configService.get<Rabbitmq>('RABBITMQ')?.GLOBAL_ENABLED) initGlobalQueues();
});
}
if (configService.get<Sqs>('SQS')?.ENABLED) initSQS(); if (configService.get<Sqs>('SQS')?.ENABLED) initSQS();

View File

@@ -4,7 +4,7 @@ import path from 'path';
import { ConfigService, Language } from '../config/env.config'; import { ConfigService, Language } from '../config/env.config';
const languages = ['en', 'pt-BR']; const languages = ['en', 'pt-BR', 'es'];
const translationsPath = path.join(__dirname, 'translations'); const translationsPath = path.join(__dirname, 'translations');
const configService: ConfigService = new ConfigService(); const configService: ConfigService = new ConfigService();

View File

@@ -22,5 +22,6 @@
"cw.contactMessage.contact": "Contact", "cw.contactMessage.contact": "Contact",
"cw.contactMessage.name": "Name", "cw.contactMessage.name": "Name",
"cw.contactMessage.number": "Number", "cw.contactMessage.number": "Number",
"cw.message.notsent": "🚨 The message could not be sent. Please check your connection. {{error}}",
"cw.message.edited": "Edited Message" "cw.message.edited": "Edited Message"
} }

View File

@@ -0,0 +1,27 @@
{
"qrgeneratedsuccesfully": "Código QR generado exitosamente!",
"scanqr": "Escanea este código QR en los próximos 40 segundos.",
"qrlimitreached": "🚨 Se alcanzó el límite de generación de QRCode. Para generar un nuevo QRCode, envíe el mensaje 'init' nuevamente.",
"numbernotinwhatsapp": "⚠️ El mensaje no fue enviado porque el contacto no es un número de Whatsapp válido..",
"cw.inbox.connected": "🚀 ¡Conexión establecida exitosamente!",
"cw.inbox.disconnect": "🚨 Instancia *{{inboxName}}* desconectado de Whatsapp.",
"cw.inbox.alreadyConnected": "🚨 La instancia {{inboxName}} está conectada.",
"cw.inbox.clearCache": "✅ Caché de la instancia {{inboxName}} borrada.",
"cw.inbox.notFound": "⚠️ Instancia {{inboxName}} no encontrada.",
"cw.inbox.status": "⚠️ Estado de la instancia {{inboxName}}: *{{state}}*.",
"cw.import.startImport": "💬 Empezando a importar mensajes. Espere por favor...",
"cw.import.importingMessages": "💬 Importando mensajes. mas un momento...",
"cw.import.messagesImported": "💬 {{totalMessagesImported}} mensajes importados. Actualiza la página para ver los nuevos mensajes..",
"cw.import.messagesException": "⚠️ Algo salió mal al importar mensajes..",
"cw.locationMessage.location": "Ubicación",
"cw.locationMessage.latitude": "Latitude",
"cw.locationMessage.longitude": "Longitude",
"cw.locationMessage.locationName": "Nombre",
"cw.locationMessage.locationAddress": "Direccion",
"cw.locationMessage.locationUrl": "URL",
"cw.contactMessage.contact": "Contacto",
"cw.contactMessage.name": "Nombre",
"cw.contactMessage.number": "Numero",
"cw.message.notsent": "🚨 El mensaje no se pudo enviar. Comprueba tu conexión. {{error}}",
"cw.message.edited": "Mensaje editado"
}

View File

@@ -22,5 +22,6 @@
"cw.contactMessage.contact": "Contato", "cw.contactMessage.contact": "Contato",
"cw.contactMessage.name": "Nome", "cw.contactMessage.name": "Nome",
"cw.contactMessage.number": "Número", "cw.contactMessage.number": "Número",
"cw.message.notsent": "🚨 Não foi possível enviar a mensagem. Verifique sua conexão. {{error}}",
"cw.message.edited": "Mensagem editada" "cw.message.edited": "Mensagem editada"
} }

View File

@@ -0,0 +1,139 @@
/**
* ┌──────────────────────────────────────────────────────────────────────────────┐
* │ @author jrCleber │
* │ @filename use-multi-file-auth-state-provider-files.ts │
* │ Developed by: Cleber Wilson │
* │ Creation date: May 31, 2024 │
* │ Contact: contato@codechat.dev │
* ├──────────────────────────────────────────────────────────────────────────────┤
* │ @copyright © Cleber Wilson 2023. All rights reserved. │
* │ Licensed under the Apache License, Version 2.0 │
* │ │
* │ @license "https://github.com/code-chat-br/whatsapp-api/blob/main/LICENSE" │
* │ │
* │ You may not use this file except in compliance with the License. │
* │ You may obtain a copy of the License at │
* │ │
* │ http://www.apache.org/licenses/LICENSE-2.0 │
* │ │
* │ Unless required by applicable law or agreed to in writing, software │
* │ distributed under the License is distributed on an "AS IS" BASIS, │
* │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
* │ │
* │ See the License for the specific language governing permissions and │
* │ limitations under the License. │
* │ │
* │ @type {AuthState} │
* │ @function useMultiFileAuthStateRedisDb │
* │ @returns {Promise<AuthState>} │
* ├──────────────────────────────────────────────────────────────────────────────┤
* │ @important │
* │ For any future changes to the code in this file, it is recommended to │
* │ contain, together with the modification, the information of the developer │
* │ who changed it and the date of modification. │
* └──────────────────────────────────────────────────────────────────────────────┘
*/
import {
AuthenticationCreds,
AuthenticationState,
BufferJSON,
initAuthCreds,
proto,
SignalDataTypeMap,
} from '@whiskeysockets/baileys';
import { isNotEmpty } from 'class-validator';
import { ProviderFiles } from '../api/provider/sessions';
import { Logger } from '../config/logger.config';
export type AuthState = { state: AuthenticationState; saveCreds: () => Promise<void> };
export class AuthStateProvider {
constructor(private readonly providerFiles: ProviderFiles) {}
private readonly logger = new Logger(AuthStateProvider.name);
public async authStateProvider(instance: string): Promise<AuthState> {
const [, error] = await this.providerFiles.create(instance);
if (error) {
this.logger.error(['Failed to create folder on file server', error?.message, error?.stack]);
return;
}
const writeData = async (data: any, key: string): Promise<any> => {
const json = JSON.stringify(data, BufferJSON.replacer);
const [response, error] = await this.providerFiles.write(instance, key, {
data: json,
});
if (error) {
// this.logger.error(['writeData', error?.message, error?.stack]);
return;
}
return response;
};
const readData = async (key: string): Promise<any> => {
const [response, error] = await this.providerFiles.read(instance, key);
if (error) {
// this.logger.error(['readData', error?.message, error?.stack]);
return;
}
if (isNotEmpty(response?.data)) {
return JSON.parse(JSON.stringify(response.data), BufferJSON.reviver);
}
};
const removeData = async (key: string) => {
const [response, error] = await this.providerFiles.delete(instance, key);
if (error) {
// this.logger.error(['removeData', error?.message, error?.stack]);
return;
}
return response;
};
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
return {
state: {
creds,
keys: {
get: async (type, ids: string[]) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: { [_: string]: SignalDataTypeMap[type] } = {};
await Promise.all(
ids.map(async (id) => {
let value = await readData(`${type}-${id}`);
if (type === 'app-state-sync-key' && value) {
value = proto.Message.AppStateSyncKeyData.fromObject(value);
}
data[id] = value;
}),
);
return data;
},
set: async (data: any) => {
const tasks: Promise<void>[] = [];
for (const category in data) {
for (const id in data[category]) {
const value = data[category][id];
const key = `${category}-${id}`;
tasks.push(value ? await writeData(value, key) : await removeData(key));
}
}
await Promise.all(tasks);
},
},
},
saveCreds: async () => {
return await writeData(creds, 'creds');
},
};
}
}

View File

@@ -6,10 +6,13 @@ import {
SignalDataTypeMap, SignalDataTypeMap,
} from '@whiskeysockets/baileys'; } from '@whiskeysockets/baileys';
import { CacheService } from '../api/services/cache.service';
import { Logger } from '../config/logger.config'; import { Logger } from '../config/logger.config';
import { RedisCache } from '../libs/redis.client';
export async function useMultiFileAuthStateRedisDb(cache: RedisCache): Promise<{ export async function useMultiFileAuthStateRedisDb(
instanceName: string,
cache: CacheService,
): Promise<{
state: AuthenticationState; state: AuthenticationState;
saveCreds: () => Promise<void>; saveCreds: () => Promise<void>;
}> { }> {
@@ -17,7 +20,7 @@ export async function useMultiFileAuthStateRedisDb(cache: RedisCache): Promise<{
const writeData = async (data: any, key: string): Promise<any> => { const writeData = async (data: any, key: string): Promise<any> => {
try { try {
return await cache.setData(key, data); return await cache.hSet(instanceName, key, data);
} catch (error) { } catch (error) {
return logger.error({ localError: 'writeData', error }); return logger.error({ localError: 'writeData', error });
} }
@@ -25,7 +28,7 @@ export async function useMultiFileAuthStateRedisDb(cache: RedisCache): Promise<{
const readData = async (key: string): Promise<any> => { const readData = async (key: string): Promise<any> => {
try { try {
return await cache.getData(key); return await cache.hGet(instanceName, key);
} catch (error) { } catch (error) {
logger.error({ localError: 'readData', error }); logger.error({ localError: 'readData', error });
return; return;
@@ -34,7 +37,7 @@ export async function useMultiFileAuthStateRedisDb(cache: RedisCache): Promise<{
const removeData = async (key: string) => { const removeData = async (key: string) => {
try { try {
return await cache.removeData(key); return await cache.hDelete(instanceName, key);
} catch (error) { } catch (error) {
logger.error({ readData: 'removeData', error }); logger.error({ readData: 'removeData', error });
} }

View File

@@ -597,6 +597,33 @@ export const archiveChatSchema: JSONSchema7 = {
required: ['archive'], required: ['archive'],
}; };
export const markChatUnreadSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
chat: { type: 'string' },
lastMessage: {
type: 'object',
properties: {
key: {
type: 'object',
properties: {
id: { type: 'string' },
remoteJid: { type: 'string' },
fromMe: { type: 'boolean', enum: [true, false] },
},
required: ['id', 'fromMe', 'remoteJid'],
...isNotEmpty('id', 'remoteJid'),
},
messageTimestamp: { type: 'integer', minLength: 1 },
},
required: ['key'],
...isNotEmpty('messageTimestamp'),
},
},
required: ['lastMessage'],
};
export const deleteMessageSchema: JSONSchema7 = { export const deleteMessageSchema: JSONSchema7 = {
$id: v4(), $id: v4(),
type: 'object', type: 'object',

View File

@@ -1,110 +0,0 @@
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="https://evolution-api.com/files/evolution-api-favicon.png" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<title>Instance Manager</title>
</head>
<body>
<div class="container mt-4">
<!-- Botão para abrir o modal de adicionar nova instância -->
<button class="btn btn-primary mb-3" data-toggle="modal" data-target="#actionModal" data-action="add">Nova
Instância</button>
<!-- Tabela de instâncias -->
<table class="table table-bordered">
<thead>
<tr>
<th>Nome da Instância</th>
<th>Status</th>
<th>API Key</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
<!-- Iterando sobre as instâncias e preenchendo a tabela -->
{{#each instances}}
<tr>
<td>{{this.instance.instanceName}}</td>
<td>{{this.instance.status}}</td>
<td>{{this.instance.apikey}}</td>
<td>
<!-- Dropdown de ações para cada instância -->
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="actionDropdown"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Ações
</button>
<div class="dropdown-menu" aria-labelledby="actionDropdown">
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#actionModal"
data-action="connect">Connect</a>
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#actionModal"
data-action="restart">Restart</a>
<!-- Adicione mais itens de ação aqui -->
<!-- ... -->
</div>
</div>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
<!-- Modal de ações -->
<div class="modal fade" id="actionModal" tabindex="-1" role="dialog" aria-labelledby="actionModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="actionModalLabel">Ação</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Fechar</button>
<button type="button" class="btn btn-primary">Salvar</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.min.js"
integrity="sha384-+sLIOodYLS7CIrQpBjl+C7nPvqq+FbNUBDunl/OZv93DB7Ln/533i8e/mZXLi/P+"
crossorigin="anonymous"></script>
<script>
$(document).ready(function(){
$('#actionModal').on('show.bs.modal', function(event) {
var button = $(event.relatedTarget);
var action = button.data('action');
console.log(action);
if (action === 'connect') {
} else if (action === 'restart') {
}
});
})
</script>
</body>
</html>