ADR-0009: Capability groups.* — contrato de gestão de grupo
- Status: aceito e implementado — todas as 14 operações (núcleo, participantes, configurações, convites/saída) nos 5 adapters existentes, mais webhooks de atualização de grupo (
GroupUpdateEvent) em 4/5 adapters (uazapi deliberadamente sem parsing estruturado, por falta de qualquer payload de exemplo — ver "Consequências"). - Data: 2026-07-11
Contexto
F2 listava "grupos, contatos" como capabilities grandes, tratadas como incrementos futuros separados (ver ADR-0008). Antes de implementar, uma pesquisa dedicada (workflow com 1 agente por provider, mesma metodologia rigorosa usada para messages.sendReaction) investigou 14 operações de gestão de grupo nos 5 adapters existentes (WAHA, Evolution GO, uazapi, Z-API, Wuzapi): createGroup, getGroupInfo, listGroups, addParticipants, removeParticipants, promoteParticipants, demoteParticipants, updateSubject, updateDescription, updatePicture, getInviteLink, revokeInviteLink, joinViaInviteLink, leaveGroup — mais a existência de eventos de webhook de atualização de grupo (o tipo GroupUpdateEvent já existe em src/core/events.ts, mas nenhum adapter o popula hoje).
Achados principais
- Cobertura excepcionalmente completa: todas as 14 operações têm endpoint HTTP confirmado (confidence alta) em todos os 5 adapters — diferente de
sendReaction, aqui não há nenhum provider que careça de suporte nativo. - Colapso de rotas é a norma, não exceção: em 3 dos 5 providers (Evolution GO, uazapi, Wuzapi),
addParticipants/removeParticipants/promoteParticipants/demoteParticipantssão UM único endpoint (POST /group/participant[s]) discriminado por um campoaction(add/remove/promote/demote). Em 3 dos 5 (Evolution GO, uazapi, Wuzapi),getInviteLinkerevokeInviteLinktambém são o mesmo endpoint, discriminado por um flagreset. Só WAHA e Z-API têm rotas de fato separadas para todas as operações. O contrato canônico expõe as 4 (e as 2) operações como métodos distintos — colapsar em uma chamada HTTP interna é responsabilidade de cada adapter (WaAdapteré "burro", só traduz). - Z-API tem um formato de ID de grupo fundamentalmente diferente dos outros 4: WAHA, Evolution GO, uazapi e Wuzapi usam o JID padrão do protocolo (
<dígitos>@g.us) — o mesmo formato queisGroupChatId/normalizeChatId(src/core/chat-id.ts) já reconhecem. Z-API usa um identificador sintético sem@:"{idNumérico}-group"(atual, desde 2021-11-04) ou o legado"{telefoneCriador}-{timestampUnix}". Isso é uma armadilha real:normalizeChatIdroteia qualquer string sem@paradigitsOnly(), que removeria o sufixo-groupe corromperia o ID. Grupo, portanto, não pode reusar cegamentenormalizeChatId— precisa de um tipo próprio (GroupIdopaco, sem normalização automática no conector) com cada adapter fazendo seu próprio map-out, análogo ao quetoZapiPhone/toWahaChatIdjá fazem para chatId de mensagem. - Confiança desigual nos webhooks de atualização de grupo (o que popularia
GroupUpdateEvent): WAHA tem payloads reais documentados (group.v2.join/leave/participants/ update, confidence alta). Evolution GO e Wuzapi (ambos baseados em whatsmeow) têm o formato reconstruído a partir do código-fonte da lib subjacente (mesma metodologia já usada no projeto para outros payloads "RECONSTRUÍDO", mas sem exemplo real capturado). uazapi confirma que a categoria de evento existe, mas não tem nenhum exemplo de payload em lugar nenhum da doc (confidence baixa no formato). Z-API não tem endpoint de config dedicado — eventos de grupo chegam pelo MESMO webhook de mensagem (ReceivedCallback), discriminados por um camponotification(GROUP_CHANGE_SUBJECT,GROUP_PARTICIPANT_ADD, ...), com exemplo literal só para 2 dos 10 valores do enum. - Foto de grupo é mais restrita que mídia de mensagem no mesmo provider: Evolution GO e Wuzapi aceitam qualquer formato de imagem em
messages.sendMedia, mas seus endpoints de foto de GRUPO checam o magic number e só aceitam JPEG (rejeitam PNG/GIF/WEBP apesar da mensagem de erro do próprio provider sugerir o contrário) — particularidade a documentar por adapter, não a impor no core. - Nomes de campo/operação divergem bastante do termo canônico (ex.:
promoteParticipantséadd-adminna Z-API,updateSubjectéupdateGroupName//group/updateNamena uazapi, várias operações em Evolution GO/Wuzapi não aparecem na doc oficial e só foram confirmadas lendo código-fonte) — mapeamento de nomes é trabalho de implementação por adapter, não do core.
Decisão
- Novo namespace
GroupsApi, análogo aInstanceApi/MessagesApi, exposto comowa.groups.*no conector via umConnectorGroupsApisempre presente (mesmo padrão dual do ADR-0008). - Toda operação de
GroupsApié opcional na interface do adapter (createGroup?,getInfo?, ...) — mesmo as 14 sendo suportadas por todos os 5 adapters atuais. Motivo: F3 vai trazer Whapi/WPPConnect/QuePasa, que podem não cobrir tudo; declarar mandatório agora e quebrar depois seria uma mudança breaking evitável. Cada operação vira sua própria capability (groups.create,groups.getInfo,groups.list,groups.addParticipants,groups.removeParticipants,groups.promoteParticipants,groups.demoteParticipants,groups.updateSubject,groups.updateDescription,groups.updatePicture,groups.getInviteLink,groups.revokeInviteLink,groups.joinViaInviteLink,groups.leave), gateada +PROVIDER_ERRORde guard-rail, igualmessages.sendReaction. GroupIdé um tipo opaco (string), sem passar pornormalizeChatId. O conector só valida não-vazio; a tradução para o formato nativo do provider (JID@g.usou o ID sintético da Z-API) é 100% responsabilidade do map-out de cada adapter.- Tipos novos em
src/core/types.ts:GroupParticipant { id, isAdmin, isSuperAdmin },GroupInfo { id, subject, description?, owner?, participants: GroupParticipant[], raw },CreateGroupInput { subject, participants },GroupInviteLink { link, raw },JoinGroupInviteInput { invite }.rawsempre presente (ADR-0002). - Link de convite é normalizado no core, diferente de
groupId:GroupInviteLink.linké SEMPRE o link completo (https://chat.whatsapp.com/<código>), mesmo quando o provider devolve só o código bare (ex.: WAHA). Diferente dogroupId(opaco, específico de cada provider), o formato do link de convite é uma constante do próprio protocolo WhatsApp — por isso as funçõesnormalizeInviteLink/extractInviteCodevivem no core (src/core/chat-id.ts), reutilizáveis por qualquer adapter, e o conector normalizaJoinGroupInviteInput.invitepara o link completo antes de chamar o adapter (mesmo padrão denormalizeChatIdparatode mensagens). - Webhooks de grupo tratados à parte: popular
GroupUpdateEventé trabalho subsequente à implementação dos métodos de envio, condicionado a validar o payload contra uma instância real quando a confiança da pesquisa for média/baixa (uazapi, e os 8/10 valores não-exemplificados denotificationna Z-API) — não travar um parser em produção com base só em reconstrução.
Consequências
- Escopo é grande o suficiente (14 operações × 5 adapters + parsing de webhook) para ser entregue em fatias, não um único PR monolítico. Fatiamento confirmado com o usuário:
- PR1 (implementado): núcleo + participantes —
create,getInfo,list,addParticipants,removeParticipants,promoteParticipants,demoteParticipants. - PR2 (implementado): configurações —
updateSubject,updateDescription,updatePicture.updatePicturerecebe umMediaRef(mesmo tipo demessages.sendMedia) commedia.kindobrigatoriamente'image'; cada adapter converte para o formato de imagem exigido pelo provider (nem todos aceitam os mesmos formatos desendMedia— ex.: Evolution GO e Wuzapi exigem data-URI com prefixo explícito em vez do base64 cru aceito porsendMedia; Wuzapi de fato só aceita JPEG, verificado por magic bytes no servidor). "Remover foto" não foi incluído (não fazia parte das 14 operações originalmente escopadas); pode ser um incremento futuro se necessário. - PR3 (implementado): convites + saída —
getInviteLink,revokeInviteLink,joinViaInviteLink,leaveGroup. Descoberta adicional: uazapi devolve o link em campos com casing diferente conforme o endpoint (invite_linksnake_case em/group/info,InviteLinkPascalCase em/group/resetInviteCode) — mesmo provider, mesma informação, nomes diferentes; documentado no dossiê para não ser confundido com erro de digitação. - Webhooks de grupo (implementado em 4/5 adapters):
GroupUpdateEventpopulado a partir do nível de confiança real de cada provider.GroupUpdateEventganhou o campoparticipants?: string[]; quando um payload reporta múltiplas mudanças simultâneas (comum em providers whatsmeow),parseWebhookemite umGroupUpdateEventpor mudança identificada (o array de retorno já suporta isso).- WAHA (alta):
group.v2.participants/update/join/leaveimplementados completos. - Evolution GO / Wuzapi (média-alta, reconstruído do código-fonte whatsmeow, sem payload real capturado): eventos
GroupInfo(diff — Join/Leave/Promote/Demote/Name/Topic) eJoinedGroupimplementados; campos sem confirmação de formato (Locked/Announce/Ephemeral/ ...) ficam fora do escopo, caem emunknown. - Z-API (parcial): só as 5 notificações de participante (
GROUP_PARTICIPANT_ADD/REMOVE/ LEAVE/PROMOTE/DEMOTE) implementadas — estrutura confirmada por analogia forte ao envelope já exemplificado para outras notificações.GROUP_CREATE/GROUP_CHANGE_SUBJECT/GROUP_CHANGE_DESCRIPTION/GROUP_CHANGE_ICON/GROUP_PARTICIPANT_INVITEdeliberadamente NÃO implementados (zero exemplo de payload) — seguem para o dispatch de mensagem comum. - uazapi (baixa): NENHUM parsing estruturado implementado — nenhum exemplo de payload de grupo existe em lugar nenhum da doc oficial; eventos
group/groupscontinuam caindo emunknown(comportamento seguro por design, não uma lacuna a "corrigir" sem validação real).
- WAHA (alta):
- PR1 (implementado): núcleo + participantes —
- Segue o padrão já estabelecido: capability opcional no adapter, sempre presente no
Connector*Apicorrespondente, guard-railPROVIDER_ERROR(ADR-0008) — reaproveitado, não uma decisão nova. - Qualquer adapter futuro (F3+) que não suporte uma operação de grupo simplesmente não a declara em
capabilities— nenhuma mudança de contrato necessária. docs/providers/<nome>.mdde cada adapter ganhou uma seção "Grupos (núcleo)" nova, com os desvios de nomenclatura/rota levantados na pesquisa (ex.: endpoint compartilhado com discriminador de ação,groupIdopaco — nota crítica na Z-API, cujo ID sintético não é um JID).