Metadata-Version: 2.5
Name: shade-plugins-whatsapp
Version: 0.1.0
Summary: Enviar mensagem de texto e de modelo pela WhatsApp Business Cloud API, sem nunca repetir um envio depois de despachado.
Author: ShadeOne
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: shade-sdk<0.23,>=0.22.1
Description-Content-Type: text/markdown

# shade-plugins-whatsapp

Envia mensagem de texto e de modelo pela [WhatsApp Business Cloud
API](https://developers.facebook.com/docs/whatsapp/cloud-api), de dentro de um robô do
[`shade-sdk`](https://pypi.org/project/shade-sdk/). Só a biblioteca padrão do Python: nenhuma
dependência além do `shade-sdk`.

No `shade.toml` do robô, o host da API vai em `[hosts] permitidos` e o token em `[credenciais]`:

```toml
[hosts]
permitidos = ["graph.facebook.com"]

[credenciais]
nomes = ["WHATSAPP_ACCESS_TOKEN"]

[plugins]
whatsapp = "0.1.0"
```

```python
import shade_plugins_whatsapp as whatsapp

# Escrita: só dentro de item.efetivar() (no ensaio não roda).
with item.efetivar("avisar o cliente"):
    whatsapp.enviar_modelo(
        item["telefone"],  # "+55 11 91234-5678": o plugin só guarda os dígitos
        "pedido_recebido",  # o modelo aprovado no painel da Meta
        "pt_BR",
        numero_id=config["whatsapp_numero"],  # o id do número de telefone da conta
        corpo=[item["cliente"], item["pedido"]],  # {{1}}, {{2}}
    )

with item.efetivar("responder a dúvida"):
    whatsapp.enviar_texto(
        item["telefone"], "Seu pedido sai hoje.", numero_id=config["whatsapp_numero"]
    )
```

## A conta

- **Token:** o token de acesso de um usuário do sistema do seu negócio no Meta Business, com a
  permissão `whatsapp_business_messaging`. Guarde-o como a credencial `WHATSAPP_ACCESS_TOKEN`; o
  plugin o lê só por `segredo()`, e ele nunca aparece em evento, log nem mensagem de erro.
- **`numero_id`:** o identificador do número de telefone da conta (não é o telefone). Não é segredo:
  guarde-o no `[config]` do robô.
- **Texto ou modelo:** o texto livre (`enviar_texto`) só chega a quem escreveu para o número nas
  últimas 24 horas. Fora dessa janela, só o modelo aprovado (`enviar_modelo`) abre a conversa. O
  modelo leva os parâmetros de texto do corpo e do cabeçalho: a lista na ordem de `{{1}}`, `{{2}}`,
  ou o dicionário `{nome: texto}` do modelo com parâmetros nomeados. Cada texto sai sem quebra de
  linha, tabulação nem quatro espaços seguidos (a API os recusa). Botão dinâmico, mídia e
  localização não são suportados nesta versão.

## O que o plugin garante

- **Nunca envia duas vezes.** A Cloud API não reconhece repetição: não há chave de idempotência, e
  um segundo pedido é uma segunda mensagem no celular. Por isso o plugin **não repete depois do
  despacho, em nenhum erro**:
  - A conexão que não chegou a abrir (recusada, nome que não resolve, rede inalcançável) é tentada
    de novo, dentro do `timeout=`, porque nada saiu.
  - A API que recusou (4xx) é `WhatsAppNaoSaiu`: nenhuma mensagem saiu. O `codigo` do erro da Meta
    vem em `erro.codigo` (por exemplo, `131047`: fora da janela de 24 horas).
  - O que sobra é `WhatsAppIncerto`: o `timeout`, a conexão que cai depois do envio, o `5xx` e a
    resposta de sucesso sem o identificador da mensagem. A mensagem **pode** estar no celular.
    O item fica retido para uma pessoa conferir (no celular de teste, no painel da Meta ou nos
    webhooks de status) e, só se a mensagem não chegou, mandar refazer. O plugin não reenvia
    sozinho, e uma nova execução do robô também não.
- **Só dentro de `item.efetivar()`.** Fora dele, a operação é um erro de contrato antes de qualquer
  byte; no ensaio, o bloco é pulado e nada é enviado.
- **Host e prazo:** só `graph.facebook.com`, com o certificado verificado, sem seguir
  redirecionamento; toda chamada aceita `timeout=` (30 s por padrão), que vale para a chamada
  inteira.
- **Sem dado pessoal em evento nem em erro:** cada chamada emite um evento com a operação, o host,
  a duração e o status. Nunca o texto, o telefone do destinatário nem o corpo da resposta; o erro
  da Meta chega só pelo código numérico.

## Operações

| Operação | Efeito | O que faz |
|---|---|---|
| `enviar_texto(para, texto, *, numero_id, previa_de_link=False)` | sim | Mensagem de texto de até 4096 caracteres. |
| `enviar_modelo(para, modelo, idioma, *, numero_id, corpo=None, cabecalho=None)` | sim | Mensagem de modelo aprovado. |

As duas devolvem `{"mensagem_id": "wamid...."}`.

## Testes

A suíte de contrato do `shade_sdk.testing.plugins` e um servidor falso da Cloud API (TLS de uma CA
de teste) acompanham o código-fonte em `tests/`. O servidor falso não reconhece repetição, como a
API de verdade.
