blog@matheusmaydana:~$

← voltar ao blog

Moderação de chat em camadas: regras, filtro de palavras, IA opcional e gente de verdade

· Projeto

O chat do MayScreen é aberto para qualquer pessoa que crie um nickname, e criar um nickname leva dois segundos. Sem moderação, é questão de tempo até alguém aparecer com xingamento, ódio, golpe ou coisa pior. E tem uma ironia que eu não queria repetir: o MayScreen existe porque o Discord perdeu o Go Live no Brasil por preocupações com a segurança de crianças e adolescentes. Colocar um chat sem cuidado nenhum no lugar seria um tanto incoerente.

O básico não depende de serviço externo nenhum: roda dentro da própria sala da transmissão, em camadas, da mais barata para a mais cara. A IA entra como reforço opcional.

As camadas

 mensagem
    │
    ▼
 1. regras de acesso      banido? silenciado? nickname novo demais?
    │                     modo lento? repetiu a mesma coisa?
    ▼
 2. filtro de palavras    sem rede, sem custo, em microssegundos
    │                     bloqueia, mascara (p****) ou deixa passar
    ▼
 3. IA (opcional)         Llama Guard 3 no Workers AI
    │                     pega ameaça e crime ditos sem palavrão
    ▼
 grava no D1 e manda para todos
    ·
    · depois, a qualquer momento:
 4. gente de verdade      apagar, silenciar, banir, registro

As três primeiras rodam na sala, um Durable Object por transmissão, antes de a mensagem ser gravada. Nada chega ao banco nem às outras telas sem passar por elas. O dono do canal e os moderadores pulam as três: quem modera não precisa ser moderado pelo filtro.

1. Regras de acesso

O filtro de palavras

É a camada que faz o trabalho pesado, e o problema não é ter uma lista de palavrões. É que ninguém escreve o palavrão do jeito que ele está na lista. Escrevem p0rr4, poooorra, p o r r a, PÔRRA. Antes de comparar qualquer coisa, o texto passa por duas dobras:

const LEET = { '0': 'o', '1': 'i', '3': 'e', '4': 'a', '5': 's', '7': 't', '@': 'a', $: 's' };

function loose(text: string): string {
  return text
    .toLowerCase()
    .normalize('NFKD')
    .replace(/\p{M}/gu, '')                  // pôrra → porra
    .replace(/[013457@$]/g, (c) => LEET[c])  // p0rr4 → porra
    .replace(/ch/g, 'x')                     // chupa → xupa
    .replace(/k/g, 'c');                     // karalho → caralho
}

// letras repetidas viram uma só, no texto e na lista de termos
function skeleton(text: string): string {
  return loose(text).replace(/(.)\1+/gu, '$1'); // poooorra → pora
}

O truque está em aplicar a mesma dobra nos termos da lista, quando o módulo carrega. “porra” e “pora” viram o mesmo esqueleto, então não importa quantas letras a pessoa esticou. A lista é escrita na grafia normal, legível, e quem lê o código não precisa pensar em leetspeak.

O que cada disfarce vira antes de comparar
EscritoComparado como
1d10t4idiota
idiiiiiotaidiota
ÍDÍÓTÁidiota
b1ch4bixa (mesmo esqueleto do termo da lista)
f.d.p, p o r r aletras soltas são coladas de volta
viado123a palavra é separada dos números

Que tipo de regra

Comparar palavra por palavra não basta. São 159 termos e 32 frases, divididos em cinco tipos de regra:

Palavra inteira
cu só bate com “cu”, nunca dentro de “curso” ou “cultura”.
Começo de palavra
idiot pega idiota, idiotas e idiotice. Só serve para radicais que não começam nenhuma palavra inocente.
Trecho
Para pouquíssimos termos longos e inconfundíveis, como “caralh” e “filhodaputa” escrito tudo junto.
Sequência
Para o que só é grave junto: “vou te matar”, “volta pra senzala”, “sei onde você mora”.
Combinação
Duas partes em qualquer lugar da linha: o nome de uma droga e “chama no pv” é oferta, enquanto o nome da droga sozinho é conversa.
// cada grupo lista alternativas; "?" marca um grupo que pode faltar
{ category: 'threat', seq: ['vou|vamos|vamo|vo|irei|quero|eu', 'te|ti', KILL] },
{ category: 'hate',   seq: ['seu|sua|seus|suas|teu|tua', 'macaco*|macaca*|gorila*'] },
{ category: 'insult', seq: ['vai|va|vao', 'tomar|toma', 'no|nu|na', 'cu|rabo'] },
{ category: 'crime',  seq: ['vendo|vende|compro|entrego|tenho|passo',
                            '?a|o|uma|um|de|da|do|boa|pura', DRUGS] },

Fora as palavras, o filtro procura padrões: CPF (formatado ou com dígito verificador válido, o que separa um CPF de um celular de 11 dígitos), telefone, e-mail, link e linha TODA EM MAIÚSCULAS.

O problema de Scunthorpe

Em 1996, moradores de Scunthorpe, na Inglaterra, não conseguiam criar conta num provedor porque o nome da cidade tem um palavrão no meio. É o maior risco de um filtro assim: barrar gente que não fez nada. Cada regra que eu escrevia, eu testava contra frases normais de chat. Alguns casos que apareceram no caminho:

Falsos positivos encontrados enquanto a lista era escrita
Frase inocenteO que pegariaComo ficou
“o viaduto tava parado”radical de um insulto homofóbicosó a palavra inteira e as variações conhecidas
“fui pra Boston”, “o porão alagou”radicais de “bosta” e “porra”palavra inteira
“tô vendo essa droga de lag”“vendo” (vender) + “droga”“vendo” só conta colado no nome de uma droga específica
“vou matar esse boss”ameaçaameaça precisa de alvo: “te”, “você”
“conteúdo infantil pros meus filhos”regra de abuso infantilsó com nude, pelada, vazado e afins
“você é um monstro nesse jogo”insulto diretoé elogio no português dos jogos; saiu da lista
“fds tô livre”abreviação de “foda-se”em mensagem é “fim de semana”; saiu da lista
“computador”contém um palavrão de quatro letraspalavrão curto só como palavra inteira

Essas frases viraram testes. São 67 casos no total, e os que precisam passar valem tanto quanto os que precisam ser barrados. Um teste que só confere bloqueios aprova um filtro que bloqueia tudo.

Níveis por canal

Um grupo de amigos adultos e uma live para crianças não querem a mesma coisa. Em qualquer canal, ódio, ameaça, incentivo a autolesão, crime e documento pessoal são bloqueados. O resto o dono escolhe:

O que cada nível faz com o resto
NívelO que acontece
LevePalavrão, xingamento, conteúdo sexual e contato passam.
PadrãoPalavrão aparece mascarado (p****). Xingamento, conteúdo sexual, telefone e e-mail são bloqueados.
RigorosoTudo isso é bloqueado, e também linhas TODAS EM MAIÚSCULAS.

Bloquear links vem ligado e vale em qualquer nível. O dono também cadastra até 100 termos próprios (spoiler, nome de alguém, o que for), que passam pela mesma dobra e pegam as mesmas variações.

Mascarar é melhor do que bloquear quando a mensagem tem conteúdo: “que p**** de lag” ainda diz o que a pessoa queria dizer. Mas só dá para mascarar uma palavra que está ali, inteira. Um palavrão soletrado (“p o r r a”) ou numa frase é bloqueado: não tem uma palavra para cobrir, e quem soletra está tentando escapar do filtro.

Três bloqueios

Quem tem uma mensagem bloqueada lê o motivo, não um “erro” genérico, e quantos bloqueios ainda pode ter: “Mais um bloqueio e você fica 10 minutos sem poder escrever.” Três bloqueios em 10 minutos silenciam a pessoa por 10 minutos, automaticamente. Uma mensagem barrada pode ser engano. Três seguidas, não, e o silêncio de 10 minutos esfria a pessoa antes de um moderador precisar entrar.

IA como reforço, não como base

Um filtro de palavras não entende frase. “Me passa teu endereço que eu vou aí resolver” não tem palavrão nenhum, e não existe lista que cubra todas as formas de ameaçar alguém. Para tentar pegar o que depende de contexto existe a terceira camada: o Llama Guard 3, um modelo da Meta feito para classificar conteúdo inseguro, rodando no Workers AI da Cloudflare. A mensagem que passou pelo filtro vai para ele, e ele responde se é segura e em quais categorias de risco cai. Bloqueio só as que importam num chat: crimes violentos e não violentos, crimes sexuais, exploração infantil, armas, ódio, autolesão e conteúdo sexual (este último liberado no nível Leve).

Algumas decisões dessa camada:

Ela é reforço, não a base
O Llama Guard não tem categoria para assédio ou xingamento comum, e isso é boa parte do que estraga um chat. O filtro de palavras continua sendo a primeira linha.
Falha aberta
Se o modelo demorar mais de 2,5 segundos, der erro ou a cota acabar, a mensagem passa. O filtro de regras já rodou, e um chat que trava cada vez que um serviço externo soluça é pior do que um chat com uma camada a menos por alguns minutos.
API REST em vez do binding
O jeito natural seria o binding de IA do Worker. Mas, no wrangler dev, ele sempre usa a conta remota, e o próprio wrangler avisa que isso pode gerar cobrança mesmo em desenvolvimento. Pela API REST, com um token opcional, o desenvolvimento local e os testes ficam isolados, e os testes simulam a resposta do modelo.
Resposta em dois formatos
A documentação não fixa o formato da saída. O código aceita texto e objeto, e qualquer outra coisa conta como “sem veredito”.
Desligada por padrão
Ela só liga com um token da Cloudflare configurado, e o dono pode desligá-la no próprio canal. O preço é de US$ 0,484 por milhão de tokens de entrada, mas o plano gratuito do Workers AI tem um limite diário que um chat movimentado gasta rápido. Por isso ela vem como opção, não como requisito.
// o modelo já respondeu "unsafe\nS1,S10" e já respondeu { safe, categories }
const response = body?.result?.response;
if (typeof response === 'string') {
  const match = /^\s*(safe|unsafe)\b([\s\S]*)$/i.exec(response);
  ...
}
if (typeof response === 'object' && typeof response.safe === 'boolean') {
  ...
}
return null; // sem veredito: a mensagem passa

Gente de verdade

Filtro nenhum substitui alguém olhando. O dono do canal escolhe até 20 moderadores. Pelo menu de cada mensagem, dono e moderadores podem:

Silenciar ou banir também apaga tudo o que a pessoa escreveu na transmissão em andamento, como na Twitch: o problema some da tela de todo mundo de uma vez. O ban vale para todas as transmissões daquele canal até alguém liberar. Moderador não age sobre o dono nem sobre outro moderador, e só o dono muda as regras e escolhe moderadores.

Tudo fica num registro: quem apagou, silenciou, baniu ou liberou quem, e o motivo de cada bloqueio automático. O que não fica é o texto da mensagem. Uma mensagem apagada que continua guardada no registro não foi apagada de verdade, e o registro só precisa explicar decisões. Ele é limpo depois de 90 dias por um cron diário, que também tira os silenciamentos vencidos.

Como as regras chegam à sala

A sala precisa saber as regras do canal a cada mensagem, e ler o banco a cada mensagem seria desperdício. Ela carrega tudo uma vez (regras, moderadores, banidos e silenciados) e guarda em memória. Quando alguém muda alguma coisa pela API, a API chama a sala ao vivo por RPC, a sala recarrega e manda para cada pessoa conectada o que mudou para ela: o aviso de que foi silenciada, o selo novo de moderador, o modo lento ligado. Se a sala hibernar, a memória some e ela recarrega na próxima mensagem. Como ela só hiberna quando está parada, não perde nada.

Nicknames e títulos

O nickname aparece em todo lugar: no chat, na lista de canais, no aviso que vai para o Discord. Por isso nicknames e nomes de transmissão ofensivos são recusados na hora de criar, com o filtro no nível mais rigoroso.

O que ainda falta

Nenhuma das camadas resolve sozinha. O filtro é rápido e de graça, mas não entende contexto. A IA entende contexto, mas custa e pode falhar. As pessoas entendem tudo, mas não estão sempre lá. Juntas, cobrem bem os buracos umas das outras.

  • moderacao
  • chat
  • cloudflare
  • workers-ai
  • durable-objects