matrix-wacloud 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrix_wacloud-0.1.0/LICENSE +21 -0
- matrix_wacloud-0.1.0/PKG-INFO +165 -0
- matrix_wacloud-0.1.0/README.md +142 -0
- matrix_wacloud-0.1.0/pyproject.toml +42 -0
- matrix_wacloud-0.1.0/setup.cfg +4 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud/__init__.py +3 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud/__main__.py +203 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud/bridge.py +288 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud/cloudapi.py +106 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud/config.py +134 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud/store.py +242 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud/util.py +67 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud/webhook.py +140 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud.egg-info/PKG-INFO +165 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud.egg-info/SOURCES.txt +22 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud.egg-info/dependency_links.txt +1 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud.egg-info/entry_points.txt +2 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud.egg-info/requires.txt +7 -0
- matrix_wacloud-0.1.0/src/matrix_wacloud.egg-info/top_level.txt +1 -0
- matrix_wacloud-0.1.0/tests/test_bridge.py +110 -0
- matrix_wacloud-0.1.0/tests/test_cloudapi.py +93 -0
- matrix_wacloud-0.1.0/tests/test_store.py +73 -0
- matrix_wacloud-0.1.0/tests/test_util_config.py +86 -0
- matrix_wacloud-0.1.0/tests/test_webhook.py +88 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Allan dos Santos Magalhães de Sousa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: matrix-wacloud
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Matrix application service that forwards room messages to WhatsApp via the official WhatsApp Business Cloud API (template messages, opt-in subscribers, delivery/read receipts).
|
|
5
|
+
Author: Allan dos Santos Magalhães de Sousa
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Source, https://github.com/allanmagalhaes/matrix-wacloud
|
|
8
|
+
Keywords: matrix,whatsapp,bridge,appservice,notifications,cloud-api
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: AsyncIO
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Communications :: Chat
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: mautrix<0.22,>=0.20
|
|
17
|
+
Requires-Dist: aiohttp>=3.9
|
|
18
|
+
Requires-Dist: PyYAML>=6.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-aiohttp>=1.0; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# matrix-wacloud
|
|
25
|
+
|
|
26
|
+
*Matrix application service that forwards messages posted in Matrix rooms to WhatsApp subscribers through the **official WhatsApp Business Cloud API** — template messages, opt-in subscriber lists, delivery/read receipts and opt-out handling.*
|
|
27
|
+
|
|
28
|
+
Conector (*bridge*) de notificação entre o [Matrix](https://matrix.org) e a WhatsApp Business Cloud API, desenvolvido como Trabalho de Conclusão de Curso do Bacharelado em Sistemas de Informação da UNIRIO.
|
|
29
|
+
|
|
30
|
+
Diferente de pontes como o mautrix-whatsapp, que emulam o WhatsApp Web num número pessoal, este pacote usa somente a API oficial da Meta. Por isso ele **não** replica conversas: ele converte avisos publicados numa sala Matrix em **mensagens por template** para os inscritos daquela sala. Essa é a forma permitida pela Meta para mensagens iniciadas pela organização.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
sala Matrix ──(appservice)──▶ matrix-wacloud ──(Cloud API)──▶ WhatsApp dos inscritos
|
|
34
|
+
▲ │ ▲
|
|
35
|
+
└──── !wa status ◀── SQLite ◀──┘ └──(webhooks: sent/delivered/read/failed)── Meta
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Funcionalidades
|
|
39
|
+
|
|
40
|
+
- Vínculo de salas Matrix a um template aprovado (`!wa vincular`).
|
|
41
|
+
- Inscritos por sala, com registro da origem e da data do consentimento (LGPD). Importação de CSV, inclusive do Google Forms.
|
|
42
|
+
- Envio concorrente com repetição automática nos erros temporários da API.
|
|
43
|
+
- Recepção dos webhooks da Meta com **validação da assinatura `X-Hub-Signature-256`**.
|
|
44
|
+
- Acompanhamento de entregas e leituras por aviso (`!wa status`).
|
|
45
|
+
- Descadastro: o inscrito responde **SAIR** no WhatsApp e é removido de todas as salas.
|
|
46
|
+
- Números mascarados nos logs e nas mensagens do bot.
|
|
47
|
+
|
|
48
|
+
## Instalação
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install matrix-wacloud
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requer Python 3.10+ e um homeserver Matrix onde você possa registrar um *application service* (por exemplo, Synapse).
|
|
55
|
+
|
|
56
|
+
## Configuração rápida
|
|
57
|
+
|
|
58
|
+
1. Copie `config.example.yaml` para `config.yaml` e `.env.example` para `.env`. Os segredos ficam no `.env`; o YAML os referencia como `${NOME}`.
|
|
59
|
+
2. Gere os tokens do appservice e cole no `.env`:
|
|
60
|
+
```bash
|
|
61
|
+
matrix-wacloud generate-tokens
|
|
62
|
+
```
|
|
63
|
+
3. Preencha no `.env` os dados do painel da Meta: `WA_PHONE_NUMBER_ID`, `WA_ACCESS_TOKEN` (token permanente), `WA_APP_SECRET` (Configurações do app → Básico → Chave secreta) e `WA_VERIFY_TOKEN` (uma senha inventada por você).
|
|
64
|
+
4. Gere o registro e adicione-o ao homeserver (`app_service_config_files` no `homeserver.yaml`):
|
|
65
|
+
```bash
|
|
66
|
+
matrix-wacloud generate-registration -c config.yaml -o registration.yaml
|
|
67
|
+
```
|
|
68
|
+
5. Teste as credenciais da Meta sem passar pelo Matrix:
|
|
69
|
+
```bash
|
|
70
|
+
matrix-wacloud send-test -c config.yaml --to 5521999999999
|
|
71
|
+
```
|
|
72
|
+
6. Execute:
|
|
73
|
+
```bash
|
|
74
|
+
matrix-wacloud run -c config.yaml
|
|
75
|
+
```
|
|
76
|
+
7. No painel da Meta (WhatsApp → Configuração → Webhooks), use `https://SEU_DOMINIO/whatsapp/webhook` como URL de callback, informe o mesmo `WA_VERIFY_TOKEN` e assine o campo **messages**. Ative também "Assinar webhooks" na conta do número que envia.
|
|
77
|
+
|
|
78
|
+
## Uso na sala
|
|
79
|
+
|
|
80
|
+
Convide `@wacloud:SEU_DOMINIO` para uma sala. Só administradores listados em `admins` podem convidar, comandar e disparar avisos.
|
|
81
|
+
|
|
82
|
+
| Comando | Efeito |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `!wa vincular <rótulo> [template] [idioma]` | liga a sala ao WhatsApp |
|
|
85
|
+
| `!wa inscrever <telefone> [origem]` | inscreve um número (ex.: `(21) 99999-1234`) |
|
|
86
|
+
| `!wa remover <telefone>` | remove um número |
|
|
87
|
+
| `!wa inscritos` | quantidade de números ativos |
|
|
88
|
+
| `!wa status` | entregas, leituras e falhas do último aviso |
|
|
89
|
+
| `!wa desvincular` | desliga a sala |
|
|
90
|
+
|
|
91
|
+
Qualquer outra mensagem de texto de um administrador numa sala vinculada é enviada aos inscritos.
|
|
92
|
+
|
|
93
|
+
Para importar inscritos de um formulário:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
matrix-wacloud import-csv -c config.yaml --room '!idDaSala:dominio' respostas.csv \
|
|
97
|
+
--phone-column Telefone --consent-column "Carimbo de data/hora"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Templates
|
|
101
|
+
|
|
102
|
+
O conteúdo do aviso entra nos parâmetros `{{1}}`, `{{2}}`... do template. A ordem é definida em `template_params`, com os valores possíveis `label` (rótulo da sala), `text` (mensagem) e `sender` (nome de quem postou). Exemplo de template **Utility** em pt_BR:
|
|
103
|
+
|
|
104
|
+
> Aviso UNIRIO ({{1}}): {{2}}
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
template_params:
|
|
108
|
+
aviso_unirio: [label, text]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Quebras de linha viram ` | `, porque a Meta rejeita parâmetros com quebra de linha. Textos longos são truncados.
|
|
112
|
+
|
|
113
|
+
## Implantação (VPS com Docker)
|
|
114
|
+
|
|
115
|
+
O `docker-compose.yml` sobe Synapse, Postgres, Caddy (HTTPS automático via Let's Encrypt) e o bridge. Do bridge, só o caminho `/whatsapp/*` fica exposto à internet.
|
|
116
|
+
|
|
117
|
+
Pré-requisitos: uma VPS com Docker e um domínio com registro DNS do tipo A apontando para ela (ex.: `avisos.seudominio.com`).
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
git clone https://github.com/allanmagalhaes/matrix-wacloud && cd matrix-wacloud
|
|
121
|
+
cp .env.example .env # preencha MATRIX_DOMAIN, POSTGRES_PASSWORD, WA_*...
|
|
122
|
+
docker compose build bridge
|
|
123
|
+
docker compose run --rm --no-deps bridge matrix-wacloud generate-tokens # cole no .env
|
|
124
|
+
|
|
125
|
+
mkdir -p data/bridge && cp config.example.yaml data/bridge/config.yaml
|
|
126
|
+
docker compose run --rm --no-deps bridge \
|
|
127
|
+
matrix-wacloud generate-registration -c /data/config.yaml -o /data/registration.yaml
|
|
128
|
+
|
|
129
|
+
# gera o homeserver.yaml do Synapse e ajusta para Postgres + appservice
|
|
130
|
+
docker compose run --rm --no-deps -e SYNAPSE_SERVER_NAME=$(grep ^MATRIX_DOMAIN .env | cut -d= -f2) \
|
|
131
|
+
-e SYNAPSE_REPORT_STATS=no synapse generate
|
|
132
|
+
docker compose run --rm --no-deps -v ./data/synapse:/synapse bridge \
|
|
133
|
+
python /app/deploy/patch_homeserver.py /synapse/homeserver.yaml
|
|
134
|
+
|
|
135
|
+
docker compose up -d
|
|
136
|
+
docker compose exec synapse register_new_matrix_user -c /data/homeserver.yaml -u allan --admin http://localhost:8008
|
|
137
|
+
docker compose logs -f bridge
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Depois, entre em <https://app.element.io> com o servidor `https://SEU_DOMINIO` e o usuário criado.
|
|
141
|
+
|
|
142
|
+
## Desenvolvimento
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python -m venv .venv && . .venv/bin/activate
|
|
146
|
+
pip install -e '.[dev]'
|
|
147
|
+
pytest
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Para o teste de integração ponta a ponta (Synapse real + bridge + uma Graph API falsa que devolve webhooks assinados):
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install matrix-synapse
|
|
154
|
+
bash scripts/e2e/run_e2e.sh
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Limitações conhecidas
|
|
158
|
+
|
|
159
|
+
- Mensagens iniciadas pela organização exigem template aprovado pela Meta e consentimento prévio do destinatário.
|
|
160
|
+
- Contas sem verificação empresarial têm limite diário de destinatários e podem sofrer restrições para outros países (erro 130497). Use um número de remetente do mesmo país dos destinatários.
|
|
161
|
+
- Respostas dos usuários não são repassadas às salas; apenas SAIR/PARAR/STOP é tratado.
|
|
162
|
+
|
|
163
|
+
## Licença
|
|
164
|
+
|
|
165
|
+
MIT
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# matrix-wacloud
|
|
2
|
+
|
|
3
|
+
*Matrix application service that forwards messages posted in Matrix rooms to WhatsApp subscribers through the **official WhatsApp Business Cloud API** — template messages, opt-in subscriber lists, delivery/read receipts and opt-out handling.*
|
|
4
|
+
|
|
5
|
+
Conector (*bridge*) de notificação entre o [Matrix](https://matrix.org) e a WhatsApp Business Cloud API, desenvolvido como Trabalho de Conclusão de Curso do Bacharelado em Sistemas de Informação da UNIRIO.
|
|
6
|
+
|
|
7
|
+
Diferente de pontes como o mautrix-whatsapp, que emulam o WhatsApp Web num número pessoal, este pacote usa somente a API oficial da Meta. Por isso ele **não** replica conversas: ele converte avisos publicados numa sala Matrix em **mensagens por template** para os inscritos daquela sala. Essa é a forma permitida pela Meta para mensagens iniciadas pela organização.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
sala Matrix ──(appservice)──▶ matrix-wacloud ──(Cloud API)──▶ WhatsApp dos inscritos
|
|
11
|
+
▲ │ ▲
|
|
12
|
+
└──── !wa status ◀── SQLite ◀──┘ └──(webhooks: sent/delivered/read/failed)── Meta
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Funcionalidades
|
|
16
|
+
|
|
17
|
+
- Vínculo de salas Matrix a um template aprovado (`!wa vincular`).
|
|
18
|
+
- Inscritos por sala, com registro da origem e da data do consentimento (LGPD). Importação de CSV, inclusive do Google Forms.
|
|
19
|
+
- Envio concorrente com repetição automática nos erros temporários da API.
|
|
20
|
+
- Recepção dos webhooks da Meta com **validação da assinatura `X-Hub-Signature-256`**.
|
|
21
|
+
- Acompanhamento de entregas e leituras por aviso (`!wa status`).
|
|
22
|
+
- Descadastro: o inscrito responde **SAIR** no WhatsApp e é removido de todas as salas.
|
|
23
|
+
- Números mascarados nos logs e nas mensagens do bot.
|
|
24
|
+
|
|
25
|
+
## Instalação
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install matrix-wacloud
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requer Python 3.10+ e um homeserver Matrix onde você possa registrar um *application service* (por exemplo, Synapse).
|
|
32
|
+
|
|
33
|
+
## Configuração rápida
|
|
34
|
+
|
|
35
|
+
1. Copie `config.example.yaml` para `config.yaml` e `.env.example` para `.env`. Os segredos ficam no `.env`; o YAML os referencia como `${NOME}`.
|
|
36
|
+
2. Gere os tokens do appservice e cole no `.env`:
|
|
37
|
+
```bash
|
|
38
|
+
matrix-wacloud generate-tokens
|
|
39
|
+
```
|
|
40
|
+
3. Preencha no `.env` os dados do painel da Meta: `WA_PHONE_NUMBER_ID`, `WA_ACCESS_TOKEN` (token permanente), `WA_APP_SECRET` (Configurações do app → Básico → Chave secreta) e `WA_VERIFY_TOKEN` (uma senha inventada por você).
|
|
41
|
+
4. Gere o registro e adicione-o ao homeserver (`app_service_config_files` no `homeserver.yaml`):
|
|
42
|
+
```bash
|
|
43
|
+
matrix-wacloud generate-registration -c config.yaml -o registration.yaml
|
|
44
|
+
```
|
|
45
|
+
5. Teste as credenciais da Meta sem passar pelo Matrix:
|
|
46
|
+
```bash
|
|
47
|
+
matrix-wacloud send-test -c config.yaml --to 5521999999999
|
|
48
|
+
```
|
|
49
|
+
6. Execute:
|
|
50
|
+
```bash
|
|
51
|
+
matrix-wacloud run -c config.yaml
|
|
52
|
+
```
|
|
53
|
+
7. No painel da Meta (WhatsApp → Configuração → Webhooks), use `https://SEU_DOMINIO/whatsapp/webhook` como URL de callback, informe o mesmo `WA_VERIFY_TOKEN` e assine o campo **messages**. Ative também "Assinar webhooks" na conta do número que envia.
|
|
54
|
+
|
|
55
|
+
## Uso na sala
|
|
56
|
+
|
|
57
|
+
Convide `@wacloud:SEU_DOMINIO` para uma sala. Só administradores listados em `admins` podem convidar, comandar e disparar avisos.
|
|
58
|
+
|
|
59
|
+
| Comando | Efeito |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `!wa vincular <rótulo> [template] [idioma]` | liga a sala ao WhatsApp |
|
|
62
|
+
| `!wa inscrever <telefone> [origem]` | inscreve um número (ex.: `(21) 99999-1234`) |
|
|
63
|
+
| `!wa remover <telefone>` | remove um número |
|
|
64
|
+
| `!wa inscritos` | quantidade de números ativos |
|
|
65
|
+
| `!wa status` | entregas, leituras e falhas do último aviso |
|
|
66
|
+
| `!wa desvincular` | desliga a sala |
|
|
67
|
+
|
|
68
|
+
Qualquer outra mensagem de texto de um administrador numa sala vinculada é enviada aos inscritos.
|
|
69
|
+
|
|
70
|
+
Para importar inscritos de um formulário:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
matrix-wacloud import-csv -c config.yaml --room '!idDaSala:dominio' respostas.csv \
|
|
74
|
+
--phone-column Telefone --consent-column "Carimbo de data/hora"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Templates
|
|
78
|
+
|
|
79
|
+
O conteúdo do aviso entra nos parâmetros `{{1}}`, `{{2}}`... do template. A ordem é definida em `template_params`, com os valores possíveis `label` (rótulo da sala), `text` (mensagem) e `sender` (nome de quem postou). Exemplo de template **Utility** em pt_BR:
|
|
80
|
+
|
|
81
|
+
> Aviso UNIRIO ({{1}}): {{2}}
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
template_params:
|
|
85
|
+
aviso_unirio: [label, text]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Quebras de linha viram ` | `, porque a Meta rejeita parâmetros com quebra de linha. Textos longos são truncados.
|
|
89
|
+
|
|
90
|
+
## Implantação (VPS com Docker)
|
|
91
|
+
|
|
92
|
+
O `docker-compose.yml` sobe Synapse, Postgres, Caddy (HTTPS automático via Let's Encrypt) e o bridge. Do bridge, só o caminho `/whatsapp/*` fica exposto à internet.
|
|
93
|
+
|
|
94
|
+
Pré-requisitos: uma VPS com Docker e um domínio com registro DNS do tipo A apontando para ela (ex.: `avisos.seudominio.com`).
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/allanmagalhaes/matrix-wacloud && cd matrix-wacloud
|
|
98
|
+
cp .env.example .env # preencha MATRIX_DOMAIN, POSTGRES_PASSWORD, WA_*...
|
|
99
|
+
docker compose build bridge
|
|
100
|
+
docker compose run --rm --no-deps bridge matrix-wacloud generate-tokens # cole no .env
|
|
101
|
+
|
|
102
|
+
mkdir -p data/bridge && cp config.example.yaml data/bridge/config.yaml
|
|
103
|
+
docker compose run --rm --no-deps bridge \
|
|
104
|
+
matrix-wacloud generate-registration -c /data/config.yaml -o /data/registration.yaml
|
|
105
|
+
|
|
106
|
+
# gera o homeserver.yaml do Synapse e ajusta para Postgres + appservice
|
|
107
|
+
docker compose run --rm --no-deps -e SYNAPSE_SERVER_NAME=$(grep ^MATRIX_DOMAIN .env | cut -d= -f2) \
|
|
108
|
+
-e SYNAPSE_REPORT_STATS=no synapse generate
|
|
109
|
+
docker compose run --rm --no-deps -v ./data/synapse:/synapse bridge \
|
|
110
|
+
python /app/deploy/patch_homeserver.py /synapse/homeserver.yaml
|
|
111
|
+
|
|
112
|
+
docker compose up -d
|
|
113
|
+
docker compose exec synapse register_new_matrix_user -c /data/homeserver.yaml -u allan --admin http://localhost:8008
|
|
114
|
+
docker compose logs -f bridge
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Depois, entre em <https://app.element.io> com o servidor `https://SEU_DOMINIO` e o usuário criado.
|
|
118
|
+
|
|
119
|
+
## Desenvolvimento
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python -m venv .venv && . .venv/bin/activate
|
|
123
|
+
pip install -e '.[dev]'
|
|
124
|
+
pytest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Para o teste de integração ponta a ponta (Synapse real + bridge + uma Graph API falsa que devolve webhooks assinados):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pip install matrix-synapse
|
|
131
|
+
bash scripts/e2e/run_e2e.sh
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Limitações conhecidas
|
|
135
|
+
|
|
136
|
+
- Mensagens iniciadas pela organização exigem template aprovado pela Meta e consentimento prévio do destinatário.
|
|
137
|
+
- Contas sem verificação empresarial têm limite diário de destinatários e podem sofrer restrições para outros países (erro 130497). Use um número de remetente do mesmo país dos destinatários.
|
|
138
|
+
- Respostas dos usuários não são repassadas às salas; apenas SAIR/PARAR/STOP é tratado.
|
|
139
|
+
|
|
140
|
+
## Licença
|
|
141
|
+
|
|
142
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "matrix-wacloud"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Matrix application service that forwards room messages to WhatsApp via the official WhatsApp Business Cloud API (template messages, opt-in subscribers, delivery/read receipts)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Allan dos Santos Magalhães de Sousa" }]
|
|
14
|
+
keywords = ["matrix", "whatsapp", "bridge", "appservice", "notifications", "cloud-api"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Framework :: AsyncIO",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Communications :: Chat",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"mautrix>=0.20,<0.22",
|
|
23
|
+
"aiohttp>=3.9",
|
|
24
|
+
"PyYAML>=6.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["pytest>=8", "pytest-aiohttp>=1.0"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
matrix-wacloud = "matrix_wacloud.__main__:main"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Source = "https://github.com/allanmagalhaes/matrix-wacloud"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
asyncio_mode = "auto"
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
filterwarnings = ["ignore:loop argument is deprecated:DeprecationWarning"]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Command line interface.
|
|
2
|
+
|
|
3
|
+
matrix-wacloud generate-tokens
|
|
4
|
+
matrix-wacloud generate-registration -c config.yaml -o registration.yaml
|
|
5
|
+
matrix-wacloud run -c config.yaml
|
|
6
|
+
matrix-wacloud import-csv -c config.yaml --room '!sala:dominio' inscritos.csv
|
|
7
|
+
matrix-wacloud send-test -c config.yaml --to 5521999999999
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import asyncio
|
|
14
|
+
import csv
|
|
15
|
+
import logging
|
|
16
|
+
import re
|
|
17
|
+
import secrets
|
|
18
|
+
import signal
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
|
|
22
|
+
import aiohttp
|
|
23
|
+
import yaml
|
|
24
|
+
|
|
25
|
+
from . import __version__
|
|
26
|
+
from .config import ConfigError, load_config
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cmd_generate_tokens(_args) -> int:
|
|
30
|
+
print(f"MWC_AS_TOKEN={secrets.token_urlsafe(48)}")
|
|
31
|
+
print(f"MWC_HS_TOKEN={secrets.token_urlsafe(48)}")
|
|
32
|
+
print("# Cole estas linhas no seu .env (nunca no git).")
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_registration(cfg) -> dict:
|
|
37
|
+
asc, domain = cfg.appservice, cfg.homeserver.domain
|
|
38
|
+
return {
|
|
39
|
+
"id": asc.id,
|
|
40
|
+
"url": asc.address,
|
|
41
|
+
"as_token": asc.as_token,
|
|
42
|
+
"hs_token": asc.hs_token,
|
|
43
|
+
"sender_localpart": asc.bot_localpart,
|
|
44
|
+
"rate_limited": False,
|
|
45
|
+
"namespaces": {
|
|
46
|
+
"users": [{"exclusive": True, "regex": f"@{re.escape(asc.bot_localpart)}:{re.escape(domain)}"}],
|
|
47
|
+
"aliases": [],
|
|
48
|
+
"rooms": [],
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cmd_generate_registration(args) -> int:
|
|
54
|
+
cfg = load_config(args.config)
|
|
55
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
56
|
+
yaml.safe_dump(build_registration(cfg), f, sort_keys=False, allow_unicode=True)
|
|
57
|
+
print(f"Registro gravado em {args.output}. Adicione-o a 'app_service_config_files' no homeserver.yaml.")
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cmd_run(args) -> int:
|
|
62
|
+
cfg = load_config(args.config)
|
|
63
|
+
logging.basicConfig(level=cfg.log_level, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
64
|
+
if cfg.log_level != "DEBUG":
|
|
65
|
+
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
|
66
|
+
from .bridge import Bridge # imported late so other commands don't need mautrix loaded
|
|
67
|
+
|
|
68
|
+
async def main() -> None:
|
|
69
|
+
bridge = Bridge(cfg)
|
|
70
|
+
stop = asyncio.Event()
|
|
71
|
+
loop = asyncio.get_running_loop()
|
|
72
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
73
|
+
try:
|
|
74
|
+
loop.add_signal_handler(sig, stop.set)
|
|
75
|
+
except NotImplementedError:
|
|
76
|
+
pass
|
|
77
|
+
await bridge.start()
|
|
78
|
+
await stop.wait()
|
|
79
|
+
await bridge.stop()
|
|
80
|
+
|
|
81
|
+
asyncio.run(main())
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _parse_consent_ts(value: str | None) -> int | None:
|
|
86
|
+
if not value:
|
|
87
|
+
return None
|
|
88
|
+
for fmt in ("%d/%m/%Y %H:%M:%S", "%d/%m/%Y %H:%M", "%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S", "%d/%m/%Y", "%Y-%m-%d"):
|
|
89
|
+
try:
|
|
90
|
+
return int(datetime.strptime(value.strip(), fmt).timestamp())
|
|
91
|
+
except ValueError:
|
|
92
|
+
continue
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cmd_import_csv(args) -> int:
|
|
97
|
+
from .store import Store
|
|
98
|
+
from .util import InvalidPhone, normalize_phone
|
|
99
|
+
|
|
100
|
+
cfg = load_config(args.config)
|
|
101
|
+
store = Store(cfg.database)
|
|
102
|
+
if not store.get_link(args.room):
|
|
103
|
+
print(f"A sala {args.room} não está vinculada. Rode '!wa vincular' nela antes.", file=sys.stderr)
|
|
104
|
+
return 2
|
|
105
|
+
added = updated = skipped = 0
|
|
106
|
+
with open(args.file, newline="", encoding="utf-8-sig") as f:
|
|
107
|
+
sample = f.read(4096)
|
|
108
|
+
f.seek(0)
|
|
109
|
+
dialect = csv.Sniffer().sniff(sample, delimiters=",;\t")
|
|
110
|
+
reader = csv.DictReader(f, dialect=dialect)
|
|
111
|
+
cols = {c.lower().strip(): c for c in (reader.fieldnames or [])}
|
|
112
|
+
phone_col = cols.get(args.phone_column.lower())
|
|
113
|
+
if phone_col is None:
|
|
114
|
+
print(f"Coluna '{args.phone_column}' não encontrada. Colunas: {reader.fieldnames}", file=sys.stderr)
|
|
115
|
+
return 2
|
|
116
|
+
consent_col = cols.get(args.consent_column.lower()) if args.consent_column else None
|
|
117
|
+
for row in reader:
|
|
118
|
+
raw = (row.get(phone_col) or "").strip()
|
|
119
|
+
if not raw:
|
|
120
|
+
skipped += 1
|
|
121
|
+
continue
|
|
122
|
+
try:
|
|
123
|
+
phone = normalize_phone(raw)
|
|
124
|
+
except InvalidPhone:
|
|
125
|
+
print(f"Ignorado (telefone inválido): {raw}", file=sys.stderr)
|
|
126
|
+
skipped += 1
|
|
127
|
+
continue
|
|
128
|
+
ts = _parse_consent_ts(row.get(consent_col)) if consent_col else None
|
|
129
|
+
if store.add_subscriber(args.room, phone, f"csv:{args.source}", ts):
|
|
130
|
+
added += 1
|
|
131
|
+
else:
|
|
132
|
+
updated += 1
|
|
133
|
+
print(f"{added} novo(s), {updated} reativado(s)/atualizado(s), {skipped} ignorado(s).")
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def cmd_send_test(args) -> int:
|
|
138
|
+
cfg = load_config(args.config)
|
|
139
|
+
from .cloudapi import CloudAPIClient, CloudAPIError
|
|
140
|
+
from .util import normalize_phone
|
|
141
|
+
|
|
142
|
+
async def main() -> int:
|
|
143
|
+
wa = cfg.whatsapp
|
|
144
|
+
async with aiohttp.ClientSession() as session:
|
|
145
|
+
client = CloudAPIClient(session, wa.phone_number_id, wa.access_token,
|
|
146
|
+
api_base=wa.api_base, api_version=wa.api_version, max_retries=0)
|
|
147
|
+
try:
|
|
148
|
+
wamid = await client.send_template(normalize_phone(args.to), args.template, args.language,
|
|
149
|
+
args.param or None)
|
|
150
|
+
except CloudAPIError as e:
|
|
151
|
+
print(f"Falhou: {e}", file=sys.stderr)
|
|
152
|
+
return 1
|
|
153
|
+
print(f"Aceito pela API: {wamid}\nVeja o resultado final (delivered/read/failed) no webhook.")
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
return asyncio.run(main())
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main(argv: list[str] | None = None) -> int:
|
|
160
|
+
p = argparse.ArgumentParser(prog="matrix-wacloud", description="Matrix → WhatsApp Cloud API bridge")
|
|
161
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
162
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
163
|
+
|
|
164
|
+
s = sub.add_parser("generate-tokens", help="gera as_token/hs_token aleatórios")
|
|
165
|
+
s.set_defaults(func=cmd_generate_tokens)
|
|
166
|
+
|
|
167
|
+
s = sub.add_parser("generate-registration", help="gera o registration.yaml para o homeserver")
|
|
168
|
+
s.add_argument("-c", "--config", required=True)
|
|
169
|
+
s.add_argument("-o", "--output", default="registration.yaml")
|
|
170
|
+
s.set_defaults(func=cmd_generate_registration)
|
|
171
|
+
|
|
172
|
+
s = sub.add_parser("run", help="executa o bridge")
|
|
173
|
+
s.add_argument("-c", "--config", required=True)
|
|
174
|
+
s.set_defaults(func=cmd_run)
|
|
175
|
+
|
|
176
|
+
s = sub.add_parser("import-csv", help="importa inscritos (com consentimento) de um CSV")
|
|
177
|
+
s.add_argument("-c", "--config", required=True)
|
|
178
|
+
s.add_argument("--room", required=True, help="ID da sala Matrix (!abc:dominio)")
|
|
179
|
+
s.add_argument("--phone-column", default="telefone")
|
|
180
|
+
s.add_argument("--consent-column", default="Carimbo de data/hora",
|
|
181
|
+
help="coluna com data/hora do consentimento (padrão do Google Forms)")
|
|
182
|
+
s.add_argument("--source", default="formulario", help="descrição da origem do consentimento")
|
|
183
|
+
s.add_argument("file")
|
|
184
|
+
s.set_defaults(func=cmd_import_csv)
|
|
185
|
+
|
|
186
|
+
s = sub.add_parser("send-test", help="envia um template direto (sem Matrix) para testar credenciais")
|
|
187
|
+
s.add_argument("-c", "--config", required=True)
|
|
188
|
+
s.add_argument("--to", required=True)
|
|
189
|
+
s.add_argument("--template", default="hello_world")
|
|
190
|
+
s.add_argument("--language", default="en_US")
|
|
191
|
+
s.add_argument("--param", action="append", help="parâmetro do corpo (repita para vários)")
|
|
192
|
+
s.set_defaults(func=cmd_send_test)
|
|
193
|
+
|
|
194
|
+
args = p.parse_args(argv)
|
|
195
|
+
try:
|
|
196
|
+
return args.func(args)
|
|
197
|
+
except ConfigError as e:
|
|
198
|
+
print(f"Erro de configuração: {e}", file=sys.stderr)
|
|
199
|
+
return 2
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
sys.exit(main())
|