ADR-0012: Capabilities messages.edit/messages.delete e novo namespace chats.*
- Status: aceito
- Data: 2026-07-12
Contexto
Após ADR-0008 (reactions), ADR-0009 (grupos) e ADR-0010 (contatos), uma pesquisa dedicada (1 agente por provider, mesma metodologia rigorosa das anteriores) investigou capabilities novas nos 8 adapters existentes (WAHA, Evolution GO, uazapi, Z-API, Wuzapi, Whapi, QuePasa, WPPConnect). Diferente das rodadas anteriores, esta pesquisa não teve um escopo fechado de operações — cada agente levantou tudo que encontrou fora do enum atual de 30 capabilities, produzindo dezenas de candidatas (enquetes, presença, labels, canais, comunidades, catálogo comercial, chamadas, status/ stories, entre outras).
Desta lista ampla, duas áreas se destacam por (a) cobertura cross-provider forte e (b) não dependerem de um tipo de conta especial (WhatsApp Business) nem de um domínio novo grande o suficiente para merecer ADR próprio:
- Editar/apagar mensagem já enviada — confirmado com confiança Alta em 7/8 (
edit) e 8/8 (delete, com 2 em confiança Média) providers. - Gestão de estado de conversa (
chats.*: arquivar, silenciar, fixar, marcar como lida) — um namespace inteiramente novo, com cobertura real bem mais desigual que grupos/contatos.
Achados centrais da pesquisa:
messages.deleteé, na prática, sempre revogação ("apagar para todos") nos providers onde o mecanismo interno foi confirmado a fundo (Evolution GO, Wuzapi, QuePasa — todos viaBuildRevoke/protocolo real do whatsmeow, sem alternativa "local"). Só o WPPConnect confirma em código um parâmetro real de escopo (onlyLocal, defaultfalse) — e o próprio exemplo de documentação do provider erra esse default (rotula um payload de "only me" que na verdade produz "apagar para todos", por omitironlyLocal: true). WAHA/uazapi/Z-API/Whapi não confirmam nem negam um modo local.messages.editnão tem janela de tempo verificável em nenhum dos 8 providers — nenhum valida localmente um prazo (o WhatsApp real limita a ~15min, mas isso é decidido do lado do cliente/servidor do WhatsApp, fora do código aberto de qualquer provider pesquisado).chats.*tem suporte muito mais desigual quegroups.*/contacts.*: Wuzapi não tem NENHUMA das 8 operações candidatas excetoarchive(busca exaustiva em código sem resultado para mute/ pin/markRead/markUnread); WAHA não documenta mute nem pin de conversa (só pin de MENSAGEM, operação diferente); Evolution GO temarchive/mute/pinmas confirma explicitamente a ausência dos endpoints inversos (unarchive/unmute) — só o par pin/unpin é simétrico lá.- Duração de mute não converge num formato único: uazapi usa um enum de horas (
0|8|168|-1), Whapi usa timestamp Unix em ms (mute_until,0= desmutar), WPPConnect usatime+typecom um bug confirmado (type: 'year'na verdade soma DIAS, não anos). Sem um formato universal confirmado, um campo de duração canônico seria inventado sem base — fica fora do escopo desta ADR (mesmo critério usado em ADR-0010 para adiargetPresence). chats.pin(conversa inteira) é operação distinta demessages.pin(mensagem dentro do chat) — as duas existem em paralelo em pelo menos WAHA (só mensagem) e Z-API (as duas, com endpoints diferentes). Esta ADR cobre sóchats.pin(conversa);messages.pin/star/forwardficam fora de escopo, candidatos a incremento futuro.chats.markRead/markUnread(chat inteiro) são operações distintas de um eventualmessages.markRead(mensagem por id) — uazapi e WPPConnect confirmam os dois mecanismos coexistindo com endpoints diferentes. Esta ADR cobre só o nível de chat.- WPPConnect tem um bug confirmado em código para
chats.pin: o controller comparastate === 'true'(string), não o booleano — enviarstate: true(como o próprio schema pede) resulta sempre em desafixar. Registrado aqui para não ser redescoberto na implementação: o adapter WPPConnect precisará serializarstatecomo a STRING"true"/"false", não booleano.
Decisão
MessagesApi.edit?/MessagesApi.delete?— opcionais, mesmo padrão desendReaction?(ADR-0008). Tipos novos emsrc/core/types.ts:EditMessageInput { to, messageId, text }→ retornaPromise<SentMessage>.DeleteMessageInput { to, messageId }→ retornaPromise<void>. Sem campo de escopo/onlyLocalnesta fase (ver "Achados" acima) — semântica assumida é sempre revogação.
- Novo namespace
ChatsApi, com TODO método opcional (mesmo padrão deGroupsApi/ContactsApi):archive?,unarchive?,mute?,unmute?,pin?,unpin?,markRead?,markUnread?— todos(chatId: string) => Promise<void>. readonly chats?: ChatsApié OPCIONAL emWaAdapter— DIFERENTE do precedente degroups/contacts(ADR-0009/0010, campo obrigatório). Dois motivos, um de processo e um técnico:- Processo: pós-v1.0,
CONTRIBUTING.mdexige issue de discussão + changesetmajorpara qualquer mudança que quebreWaAdapter. Um campo obrigatório novo quebraria a compilação dos 8 adapters existentes — exatamente o gatilho que a regra foi desenhada para pegar. Um campo opcional é 100% aditivo: nenhum adapter existente precisa mudar. - Técnico: a cobertura real por provider é irregular demais para um campo mandatório fazer sentido aqui (ver "Achados" — Wuzapi não tem quase nada do namespace). Forçar todo adapter a ter um objeto
chats: {...}com a maioria dos métodosundefinednão reflete cobertura real nem ajuda o consumidor — um campo ausente é mais honesto que um objeto quase vazio. - Não há razão técnica para preferir o padrão antigo: o guard-rail do conector (
callChatsMethod) já precisa checar método-ausente por método; estender isso para também tolerar objeto-inteiro-ausente é uma troca de.por?., sem custo adicional de design.
- Processo: pós-v1.0,
chatIddechats.*passa pornormalizeChatIdno conector — mesmo tratamento decontacts.*(ADR-0010), NÃO o tratamento opaco degroupId(ADR-0009). Um "chat" é o mesmo alvo endereçável demessages.sendText, podendo ser indivíduo ou grupo via JID explícito.- Cada par archive/unarchive, mute/unmute, pin/unpin, markRead/markUnread vira 2 capabilities separadas, não 1 capability com parâmetro booleano — mesmo padrão de
contacts.block/unblock. Evidência concreta (não só estilo): Evolution GO temarchivemas confirma a ausência deunarchive— só com capabilities separadas o adapter consegue declarar o suporte real (parcial) sem mentir sobre a metade que falta. ConnectorChatsApi/callChatsMethodseguem o mesmo padrão dual deConnectorGroupsApi/callGroupsMethod: todo método sempre presente no conector, gateado por capability +PROVIDER_ERRORquando o adapter declara sem implementar (incluindo o caso deadapter.chatsinteiro ausente, coberto via optional chaining).ConnectorMessagesApi.edit/deleteseguem o mesmo padrão; o guard-rail antes inline desendReactioné generalizado para um helpercallMessagesMethod(refatoração interna, sem mudança de comportamento observável — mesmo texto de erro).
Justificativa
- Reaproveita integralmente o padrão dual já validado 3 vezes (
sendReaction/groups.*/contacts.*) — nenhuma decisão arquitetural nova além do ponto 3 (campo opcional em vez de obrigatório), que é uma correção de curso justificada pela regra de breaking change pós-v1.0 criada depois que ADR-0009/0010 foram aceitas. - Não incluir escopo de delete (
onlyLocal) nem duração de mute evita inventar campos sem confiança cross-provider suficiente — mesmo critério que já levou ADR-0010 a adiargetPresenceem vez de forçar um shape ruim. - Separar
chats.pin/markReaddemessages.pin/markRead(mensagem-a-mensagem) evita colisão semântica confirmada na própria pesquisa (uazapi e WPPConnect documentam os dois mecanismos como operações distintas do protocolo).
Consequências
- Escopo fatiado em PRs, mesmo espírito de ADR-0009/0010, por ordem de cobertura real:
messages.edit/messages.delete(maior cobertura combinada).chats.archive/chats.unarchive.chats.markRead/chats.markUnread.chats.pin/chats.unpin(WPPConnect precisa do workaround de string'true'/'false').chats.mute/chats.unmute(cobertura mais fraca e mais inconsistente).
MockAdapter(src/testing/mock-adapter.ts) implementamessages.edit/deletee um objetochatsem memória (mesmo estilo deblockedIds/contactsById) na mesma mudança que adiciona as entradas ao enumCAPABILITIES—MockAdapterOptions.capabilitiesdefault para o enum inteiro, então o enum crescer sem oMockAdapterimplementar quebraria o próprio guard-rail dePROVIDER_ERRORna suite de contrato.docs/providers/<nome>.mdde cada adapter ganha seções novas ("Edição/exclusão de mensagem", "Conversas (chats.*)") durante a implementação, documentando por adapter as ausências (unarchive/unmuteno Evolution GO, o namespacechats.*quase todo ausente no Wuzapi, o bug destate === 'true'no WPPConnect, etc.).messages.star,messages.forward,messages.pin(nível de mensagem),presence.*,labels.*,channels.*,communities.*,business.*,calls.*,stories.*— todos encontrados na pesquisa com força variável — ficam explicitamente fora desta ADR, candidatos a ADRs dedicados futuros (mesmo padrão de ADR-0010 adiandogetPresence).- Nenhuma issue de discussão prévia foi tecnicamente exigida por este desenho: por ser aditivo em toda a superfície (campo
chats?opcional, métodosedit?/delete?opcionais, novas entradas de union emCAPABILITIES), não há quebra de compatibilidade que acione o gate doCONTRIBUTING.md. Changesetminor.
Alternativas consideradas
readonly chats: ChatsApiobrigatório (mesmo padrão degroups/contacts): rejeitado — quebraria compilação dos 8 adapters, acionando o gate de breaking change pós-v1.0 sem necessidade técnica (ver "Decisão" item 3).chats.archive(chatId, archived: boolean)(1 capability, parâmetro booleano): rejeitado — Evolution GO temarchivefuncional semunarchive; uma única capability booleana obrigaria o adapter a mentir sobre suporte total ou declarar nada.DeleteMessageInput.forEveryone?: boolean: rejeitado nesta fase — só WPPConnect confirma a distinção em código; os demais 7 providers não têm essa opção confirmada.ChatMuteInput { chatId, durationSeconds? }: rejeitado nesta fase — nenhum formato de duração converge entre os providers que suportam mute; ver "Achados".