AC

Adam Cole01/09/2026 11:43
Compartilhe

Como evitar spam e raids no Discord sem falsos positivos

    Criar um bot que bane spam é fácil.

    Criar um bot que consegue pegar spam, raids e comportamento destrutivo sem começar a punir usuário normal é outra coisa.

    Se alguém manda a mesma mensagem três vezes, pode ser spam.

    Também pode ser uma pessoa perguntando a mesma coisa porque ninguém respondeu.

    Se uma conta foi criada hoje, pode ser raid.

    Também pode ser alguém que literalmente acabou de criar Discord.

    Então não faz:

    conta nova = ban
    

    É melhor juntar contexto.

    conta nova
    + sem avatar
    + entrou junto com várias contas
    + username parecido
    + começou a repetir link
    = agora temos alguma coisa
    

    É basicamente assim que eu acho que um sistema de moderação deveria ser pensado.

    Neste artigo vou passar pela arquitetura que uso como referência trabalhando no BetterAntispam, mas o código e a lógica servem para qualquer bot em Node.js usando discord.js ou outro wrapper da API do Discord.

    A parte importante não é qual bot você usa.

    É como você toma a decisão.

    Por que antispam simples começa a quebrar rápido

    Um antispam básico normalmente começa assim:

    if (messagesLastFiveSeconds >= 5) {
    await member.timeout(30 * 60 * 1000)
    }
    

    Funciona.

    Até não funcionar.

    Imagina um canal muito ativo durante um anúncio.

    Um usuário manda:

    não
    não
    espera
    deixa eu ver
    achei
    

    Cinco mensagens.

    Seu código olha isso e fala:

    SPAM
    

    Mas não teve ataque nenhum.

    O problema é tratar frequência como intenção.

    Frequência é um sinal.

    Só isso.

    Eu prefiro separar vários detectores pequenos:

    const signals = {
    frequency: false,
    duplicateMessages: false,
    massMentions: false,
    suspiciousLinks: false,
    repeatedInvites: false,
    accountAge: false
    }
    

    Cada detector responde uma coisa específica.

    Depois outra parte do sistema decide o que fazer com aquilo.

    Primeiro usa o que o Discord já oferece

    Não precisa recriar coisa que o Discord já consegue fazer direito.

    O Discord tem AutoMod para bloquear conteúdo e detectar algumas formas de spam.

    Usa.

    É bom.

    Deixa a camada nativa pegar os casos simples e usa seu bot para comportamento que precisa de mais contexto.

    Eu separaria assim:

    Discord AutoMod
        ↓
    mensagem individual
    
    Seu antispam
        ↓
    histórico de comportamento
    
    Antiraid
        ↓
    comportamento entre várias contas
    
    Antinuke
        ↓
    ações administrativas perigosas
    

    Isso também evita colocar toda responsabilidade de segurança em um processo Node.js.

    Se seu bot reiniciar por 15 segundos, AutoMod ainda existe.

    Não mistura detecção com punição

    Isso faz muita diferença quando você precisa ajustar falso positivo depois.

    Evita isso:

    if (isSpam(message)) {
    await message.member.ban()
    }
    

    Seu detector virou juiz e executor.

    Faz algo mais parecido com:

    const result = detectSpam(message, state)
    
    const decision = evaluateSignals(result)
    
    await executeAction(decision)
    

    Agora você tem três partes:

    detectar
    ↓
    decidir
    ↓
    executar
    

    Se amanhã você descobrir que links estão recebendo peso demais, você altera decisão.

    Não precisa reescrever o detector.

    Se quiser trocar ban por quarentena, muda execução.

    Não precisa mexer na lógica de spam.

    Isso parece detalhe até você ter 20 detectores diferentes.

    Mantendo estado por usuário

    Pra analisar comportamento você precisa lembrar o que aconteceu antes.

    Um exemplo simples:

    const memberStates = new Map()
    
    function getMemberState(userId) {
    if (!memberStates.has(userId)) {
      memberStates.set(userId, {
        messages: [],
        links: [],
        mentions: [],
        warnings: []
      })
    }
    
    return memberStates.get(userId)
    }
    

    Quando chega mensagem:

    client.on('messageCreate', async message => {
    if (!message.guild || message.author.bot) return
    
    const state = getMemberState(message.author.id)
    
    state.messages.push({
      content: message.content,
      timestamp: Date.now()
    })
    })
    

    Mas não deixa isso crescer pra sempre.

    Remove dados antigos.

    const WINDOW = 30_000
    
    state.messages = state.messages.filter(
    message => message.timestamp > Date.now() - WINDOW
    )
    

    Pronto.

    Agora você tem uma janela dos últimos 30 segundos.

    Pode medir frequência, repetição e outros padrões.

    Em produção provavelmente você vai querer uma estrutura mais eficiente, mas a ideia é essa.

    Detectando mensagem repetida direito

    Não compara só string exata.

    Isso aqui:

    ENTRA AQUI https://example.com/a
    
    entra aqui https://example.com/b
    
    entra aqui!!! https://example.com/c
    

    é praticamente a mesma mensagem.

    Mas:

    a === b
    

    vai retornar falso.

    Então normaliza.

    function normalizeMessage(content) {
    return content
      .toLowerCase()
      .trim()
      .replace(/\s+/g, ' ')
      .replace(/[!?.,]+/g, '')
    }
    

    Depois você pode remover URLs antes de comparar o corpo:

    function removeUrls(content) {
    return content.replace(/https?:\/\/\S+/gi, '')
    }
    

    Agora:

    const normalized = normalizeMessage(
    removeUrls(message.content)
    )
    

    Isso já pega muito spam preguiçoso.

    Se quiser ir além, compara similaridade.

    Mas não sai colocando distância de Levenshtein em tudo e banindo usuário porque:

    bom dia pessoal
    

    é parecido com:

    bom dia gente
    

    De novo, combina sinal.

    Usa score quando um único sinal não basta

    Eu gosto bastante de score para raid porque raid normalmente é combinação de coisas.

    Exemplo:

    function getMemberRisk(member) {
    let score = 0
    
    const accountAge =
      Date.now() - member.user.createdTimestamp
    
    if (accountAge < 60 * 60 * 1000) {
      score += 20
    }
    
    if (!member.user.avatar) {
      score += 10
    }
    
    return score
    }
    

    Agora adiciona contexto global.

    if (joinsLastMinute > 20) {
    score += 20
    }
    

    Se usernames estão estranhamente parecidos:

    if (similarUsernameCluster) {
    score += 15
    }
    

    E se depois do join começou spam:

    if (repeatedSpamAfterJoin) {
    score += 30
    }
    

    Agora você consegue ter algo assim:

    0-20
    normal
    
    20-40
    log
    
    40-60
    quarentena
    
    60+
    ação mais forte
    

    Não copia esses números.

    Eles são exemplo.

    Pega o comportamento do seu servidor e calibra.

    Quarentena resolve muita coisa

    Se seu detector tem 70% de certeza que alguém é malicioso, ban permanente pode ser agressivo demais.

    Então não transforma:

    suspeito
    

    em:

    ban para sempre
    

    Pode transformar em:

    suspeito
    ↓
    remove acesso
    ↓
    mantém usuário no servidor
    ↓
    avisa staff
    ↓
    coleta mais contexto
    

    Isso é quarentena.

    No BetterAntispam eu uso bastante esse modelo porque ele deixa os detectores mais sensíveis sem transformar qualquer erro numa punição irreversível.

    Você pode criar uma role:

    Quarantined
    

    e negar acesso aos canais normais.

    Por exemplo:

    async function quarantineMember(member, roleId) {
    await member.roles.set([roleId])
    }
    

    Na implementação real você provavelmente vai querer preservar estado anterior para restaurar depois.

    Não joga os cargos antigos fora sem guardar nada.

    Raid não é só quantidade de joins

    Esse é outro erro comum.

    if (joinsLastMinute >= 20) {
    // RAID
    }
    

    Não necessariamente.

    Um streamer pode mandar convite e 200 pessoas legítimas entram.

    Então mede a composição da onda.

    Mantém os joins:

    const guildJoins = new Map()
    
    function recordJoin(guildId, member) {
    if (!guildJoins.has(guildId)) {
      guildJoins.set(guildId, [])
    }
    
    const joins = guildJoins.get(guildId)
    
    joins.push({
      userId: member.id,
      username: member.user.username,
      createdAt: member.user.createdTimestamp,
      hasAvatar: Boolean(member.user.avatar),
      joinedAt: Date.now()
    })
    }
    

    Depois analisa a janela.

    Pergunta:

    quantos entraram?
    quantas contas são novas?
    quantas não têm avatar?
    os nomes parecem relacionados?
    estão fazendo a mesma coisa depois do join?
    

    20 contas entrando não diz tanto.

    20 contas entrando e 18 criadas nos últimos 30 minutos é outra história.

    Antiraid também precisa de estado global

    Até aqui um Map() funciona bem pra exemplo.

    Agora imagina seu bot crescendo.

    Você tem:

    processo 1
    processo 2
    processo 3
    processo 4
    

    Ou vários shards.

    Ou várias máquinas.

    Esse código:

    const guildJoins = new Map()
    

    não é mais estado global.

    Cada processo enxerga o próprio pedaço.

    Então talvez um processo veja:

    6 joins
    

    outro:

    7 joins
    

    outro:

    8 joins
    

    Nenhum acha estranho.

    Mas no total entraram 21 contas.

    Aí usa algum estado compartilhado.

    Redis é uma opção óbvia.

    Discord Gateway
        ↓
    shards
        ↓
    Redis
        ↓
    detector de raid
        ↓
    ação
    

    Pode usar outra coisa.

    Só não assume que memória local continua sendo verdade global quando sua arquitetura deixa de ser um processo.

    Antinuke precisa ser separado de antispam

    Agora vem uma ameaça completamente diferente.

    Um administrador comprometido.

    Essa pessoa pode nunca mandar uma mensagem.

    Ela só faz:

    delete channel
    delete channel
    ban
    ban
    delete role
    create webhook
    change permission
    

    Seu melhor detector de spam do mundo não ajuda.

    Para isso você precisa observar eventos administrativos e Audit Log.

    Faz limites por tipo de ação.

    Por exemplo:

    const destructiveLimits = {
    channelDelete: {
      count: 3,
      window: 10_000
    },
    
    roleDelete: {
      count: 3,
      window: 10_000
    },
    
    memberBan: {
      count: 5,
      window: 10_000
    }
    }
    

    Não usa um limite único.

    Apagar um canal não tem necessariamente o mesmo risco que alterar uma permissão crítica.

    E tem outro detalhe:

    bot legítimo pode fazer ações em massa.

    Então precisa de allowlist.

    const trustedIds = new Set([
    'BOT_ID',
    'ADMIN_ID'
    ])
    

    Mas também não transforma allowlist numa lista de 40 pessoas porque aí você basicamente desligou o sistema.

    Rate limit faz parte do sistema de segurança

    Durante uma raid seu bot recebe mais eventos.

    Aí ele quer:

    • apagar mensagens
    • dar timeout
    • aplicar cargos
    • editar canais
    • banir
    • mandar logs
    • mudar Slowmode

    Tudo ao mesmo tempo.

    É exatamente quando você menos quer uma fila mal feita.

    Então não faz:

    await Promise.all(
    attackers.map(user => guild.members.ban(user))
    )
    

    e espera que fique tudo bom.

    Bibliotecas como discord.js já ajudam com os rate limits da API, mas sua lógica ainda precisa controlar prioridade e carga.

    Eu faria fila lógica.

    prioridade 1
    conter ataque
    
    prioridade 2
    impedir novas ações
    
    prioridade 3
    remover atacante
    
    prioridade 4
    limpeza
    
    prioridade 5
    logs secundários
    

    O embed bonito no canal de log não pode ser mais importante que fechar o canal que está sendo destruído.

    Não hardcode rate limit da API do Discord

    Também não faz:

    const DISCORD_LIMIT = 50
    

    e constrói sua arquitetura inteira assumindo que todo endpoint funciona igual.

    A API do Discord usa rate limits diferentes por rota e retorna informações de bucket e tempo de retry.

    Deixa a biblioteca resolver o que ela já resolve e, se você estiver trabalhando direto com HTTP, respeita os headers e retry_after.

    Run whatever requests you want, é bom, mas respeita o bucket.

    429 não deveria ser parte normal da sua lógica de aplicação.

    Lockdown é estado, não só comando

    Quando raid é confirmada, o comportamento do servidor deveria mudar.

    Em vez de continuar:

    modo normal
    modo normal
    modo normal
    

    enquanto 100 contas entram, muda para:

    RAID MODE
    

    Aí temporariamente você pode:

    • aumentar threshold de verificação
    • restringir usuários novos
    • ativar Slowmode
    • bloquear canais
    • aumentar sensibilidade
    • pausar algumas automações
    • avisar moderadores

    Depois volta.

    Isso permite deixar as regras normais menos agressivas.

    Você não precisa tratar todo usuário como atacante 24 horas por dia só porque uma raid pode acontecer um dia.

    Recuperação também é segurança

    Muita arquitetura para no:

    ataque bloqueado
    

    Mas talvez o ataque já tenha mandado 3.000 mensagens.

    Agora quem limpa?

    No BetterAntispam existe /nuke e ferramentas em massa como /slowmodebulk, mas o princípio vale para qualquer implementação.

    O workflow completo é:

    detectar
    ↓
    conter
    ↓
    remover
    ↓
    limpar
    ↓
    restaurar estado normal
    

    Pensa no pós-incidente enquanto você está fazendo a prevenção.

    Não depois.

    Três abordagens que eu compararia

    1. Só AutoMod do Discord

    Bom para:

    • palavras
    • spam simples
    • menções
    • regras básicas

    Vantagem:

    menos código
    menos dependência
    

    Desvantagem:

    menos contexto customizado
    

    Eu usaria de qualquer forma.

    2. Bot tradicional de moderação

    Bom para:

    • warn
    • mute
    • timeout
    • ban
    • logs
    • comandos de staff

    É suficiente para muita comunidade.

    Mas não assume que todo bot de moderação automaticamente é um sistema antiraid.

    São requisitos diferentes.

    3. Sistema contextual

    É onde você começa a juntar:

    eventos
    +
    histórico
    +
    score
    +
    estado global
    +
    ação progressiva
    

    É a arquitetura que eu prefiro para BetterAntispam.

    Mais trabalho.

    Também te dá muito mais controle sobre falso positivo.

    Como eu começaria um bot desses em Node.js

    Primeiro:

    npm install discord.js
    

    Depois cria detectores separados.

    Estrutura mais ou menos:

    src/
    ├── detectors/
    │   ├── messageFrequency.js
    │   ├── duplicateMessage.js
    │   ├── mentionSpam.js
    │   ├── raidScore.js
    │   └── destructiveAction.js
    │
    ├── decisions/
    │   └── moderationDecision.js
    │
    ├── actions/
    │   ├── timeout.js
    │   ├── quarantine.js
    │   ├── ban.js
    │   └── lockdown.js
    │
    └── state/
      ├── memberState.js
      └── guildState.js
    

    Não precisa usar exatamente isso.

    Só evita um:

    index.js
    

    com 4.000 linhas onde evento, regra, banco, punição e embed estão todos misturados.

    Detecta em um lugar.

    Decide em outro.

    Executa em outro.

    Como testar sem descobrir os problemas durante uma raid

    Cria testes de comportamento.

    Não testa só função isolada.

    Exemplo:

    describe('raid scoring', () => {
    it('does not punish a single fresh account', () => {
      // ...
    })
    
    it('raises risk during coordinated fresh-account joins', () => {
      // ...
    })
    
    it('does not classify a normal join spike as a raid', () => {
      // ...
    })
    })
    

    Testa caso legítimo também.

    Isso é importante.

    Todo mundo testa:

    spammer deve ser pego
    

    Pouca gente testa:

    usuário normal NÃO deve ser pego
    

    Falso positivo é requisito.

    Trata como um.

    Métricas que eu guardaria

    Se você quer melhorar o sistema depois, salva:

    quantos eventos foram detectados
    quantos usuários foram punidos
    qual regra disparou
    qual era o score
    quantas punições foram revertidas
    quais regras staff desativou
    quais exceções foram adicionadas
    

    Se staff vive adicionando exceção para uma regra, talvez a regra esteja ruim.

    Se 30% das quarentenas são revertidas, seu threshold provavelmente está agressivo.

    Não melhora detector no feeling.

    Mede.

    O que eu procuraria em qualquer solução de moderação

    Se eu fosse avaliar uma implementação eu olharia:

    Threshold configurável

    Servidor diferente tem comportamento diferente.

    Detecção separada

    Spam, raid e ação administrativa não deveriam ser uma única regra.

    Exceções

    Usuários, roles, canais e bots legítimos precisam de tratamento diferente.

    Ações progressivas

    log
    ↓
    timeout
    ↓
    quarentena
    ↓
    kick
    ↓
    ban
    

    Estado compartilhado

    Importante quando a aplicação escala horizontalmente.

    Lockdown

    Resposta rápida quando o comportamento global muda.

    Observabilidade

    Precisa explicar por que tomou uma ação.

    Recuperação

    Não adianta só impedir dano novo.

    Precisa lidar com o que já aconteceu.

    Onde o BetterAntispam entra nisso

    Eu trabalho no BetterAntispam e boa parte dessas decisões veio justamente de ter que resolver esses problemas em um bot real.

    Ele separa antispam, antiraid, antinuke, verificação, quarentena e lockdown em sistemas diferentes.

    Não estou colocando aqui como:

    use esse bot
    

    A parte interessante é o desenho.

    Você pode implementar exatamente a mesma ideia no seu próprio projeto em Node.js.

    Na verdade eu recomendo pegar a arquitetura e fazer do jeito que fizer sentido para sua aplicação.

    Detecta os sinais disponíveis.

    Mantém estado suficiente.

    Combina contexto.

    Escolhe resposta proporcional.

    Mede falso positivo.

    Depois ajusta.

    Conclusão

    Moderação automática não deveria tentar responder:

    esse usuário é ruim?
    

    Essa pergunta é difícil demais.

    Faz perguntas menores:

    ele está mandando mensagem rápido?
    as mensagens são parecidas?
    a conta é nova?
    entrou durante uma onda?
    outras contas parecem relacionadas?
    começou comportamento suspeito depois?
    

    Cada resposta sozinha é fraca.

    Juntas começam a formar contexto.

    Usa AutoMod para o básico.

    Separa antispam de antiraid.

    Separa ambos de antinuke.

    Não mistura detecção com punição.

    Usa quarentena quando ainda existe dúvida.

    Distribui estado quando seu bot começa a escalar.

    Respeita rate limit.

    E mede falso positivo como métrica principal, não como detalhe.

    Porque fazer um bot que não deixa nenhum spam passar é fácil.

    Bloqueia todo mundo.

    O problema bom de resolver é deixar os usuários normais conversarem enquanto o sistema fica quieto no fundo e só aparece quando realmente precisa.

    Referências técnicas

    • Discord Developer Documentation — Rate Limits
    • Discord Developer Documentation — Audit Logs
    • Discord Safety — Auto Moderation in Discord
    • discord.js
    • BetterAntispam — projeto usado como exemplo de implementação

    Transparência: trabalho no BetterAntispam. Ele foi citado como exemplo de arquitetura utilizada em produção; este artigo não é uma oferta comercial e os conceitos podem ser implementados independentemente em qualquer bot Discord.

    Compartilhe
    Comentários (0)