São duas camadas, no molde Stream/Sendbird: o app declara o teto; cada conversa liga um subconjunto. Bloquear e silenciar são da pessoa, não da sala.
Teto no Chat App
O produto declara o que pode existir:
PATCH /v1/chat/apps/{id} (escopo chat:apps:manage) altera o teto. Não existe “a conversa ligou vídeo se o app não tem vídeo”.
Interruptor na conversa
Serializado em GET/list:
Create de conversa aplica o default do app; o body pode passar um subconjunto. PATCH /v1/chat/conversations/{id} muda o interruptor e emite conversation.updated (WebSocket) e chat.conversation.updated (webhook).
Quem muda a conversa
- API Key: sempre
AGENT: sempre
GROUP + OWNER (e ADMIN): sim
USER em DIRECT: só se allowUserManageCapabilities estiver ligado
Envio recusado
O composer do app do cliente só esconde botão. Quem manda é o servidor:
- Tipo fora da sala ou do teto →
CHAT_CAPABILITY_DISABLED (403)
- DIRECT com block entre os
USER → CHAT_USER_BLOCKED (403)
O agente também respeita a sala. O mesmo vale no REST e no WebSocket (message.send com type + mediaUrl). recording.start exige audio ligado.
Exemplo suporte: o app permite texto, imagem, áudio e arquivo. A conversa do pedido nasce só texto. O agente (ou a API Key) liga mídia naquela sala. Desligar depois volta a recusar IMAGE / AUDIO / FILE.
Silenciar
POST /v1/chat/conversations/{id}/mute
- JWT: silencia a si mesmo
- API Key: informe
memberExternalUserId
mutedUntil nulo ou muted: false desliga. Membro silenciado ainda envia e lê; o campo vai em members[].mutedUntil. Evento WebSocket: member.updated.
Bloquear
Tabela por Chat App (PUT / DELETE / GET /v1/chat/blocks/{externalUserId}).
- Não cria DIRECT com o bloqueado
- Não adiciona o bloqueado em grupo (e o bloqueado não entra se
joinPolicy=OPEN)
- DIRECT já existente: envio recusado nos dois sentidos
- Não apaga o histórico
USER bloqueia USER. Não dá para bloquear um AGENT
- Com API Key, envie
chatAppId e blockerExternalUserId
Eventos: WebSocket user.blocked / user.unblocked nas conversas DIRECT afetadas; webhooks chat.user.blocked / chat.user.unblocked.
Remover membro continua sendo “tirar da sala”, não block global.
Próximos passos