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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """matrix-wacloud: Matrix -> WhatsApp Business Cloud API notification bridge."""
2
+
3
+ __version__ = "0.1.0"
@@ -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())