---
title: "Sistema de sugestões no Discord com votação, aprovação e thread"
description: "Monte um sistema de sugestões com discord.js v14: /sugerir, votos por botões sem voto duplo, aprovação com motivo, embed que muda de cor e thread de debate."
url: "https://streethosting.com.br/guias/bots-discord/bot-discord-sistema-sugestoes"
category: "bots-discord"
slug: "bot-discord-sistema-sugestoes"
datePublished: "2026-11-13"
dateModified: "2026-11-13"
author: "Equipe StreetHosting"
difficulty: "intermediario"
language: "pt-BR"
keywords:
  - "sistema de sugestões discord"
  - "bot discord sugestões"
  - "comando sugerir discord.js"
  - "votação com botões discord"
  - "aprovar recusar sugestão discord"
---

# Sistema de sugestões no Discord: /sugerir, votos, aprovação da equipe e thread

Um canal de sugestões organizado: a pessoa usa /sugerir, o bot posta um embed com botões de voto, abre uma thread para a conversa e a equipe aprova ou recusa com motivo, trocando a cor do embed.

> **Resposta rápida**
>
> O comando `/sugerir` posta um embed no canal de sugestões com dois botões de voto e abre uma thread na mensagem para a discussão. O bot guarda, por sugestão, a lista de quem votou em cada lado, o que impede voto duplo. A equipe usa `/sugestao aprovar` ou `/sugestao recusar` com um motivo, e o bot edita o embed trocando a cor e desativando os botões.

## Como o sistema funciona

Sugestões soltas em um canal de conversa se perdem em minutos. O sistema que vamos montar dá a cada ideia um lugar, uma contagem de votos e uma resposta oficial. O fluxo tem quatro passos:

1. A pessoa usa `/sugerir` e escreve a ideia.
2. O bot publica um embed no canal de sugestões, numerado, com os botões Concordo e Discordo, e abre uma thread nessa mensagem.
3. Os membros votam nos botões e debatem na thread. Cada pessoa tem um voto, que pode ser trocado.
4. A equipe aprova ou recusa com um motivo. O embed muda de cor, mostra a resposta e os botões são desativados.

Tudo isso usa só slash commands, botões e embeds, então a intent `Guilds` basta. Se algum desses conceitos for novo, os guias de [botões e menus interativos](https://streethosting.com.br/guias/bots-discord/bot-discord-botoes-menus-interativos) e de [embeds](https://streethosting.com.br/guias/bots-discord/bot-discord-embeds-mensagens-formatadas) explicam a base.

## Canal, permissões e comandos

Crie um canal de texto só para sugestões e copie o ID dele para a variável de ambiente `SUGESTOES_CANAL_ID`. Dica de organização: tire a permissão de enviar mensagens do cargo `@everyone` nesse canal. Como só o bot posta, o canal fica limpo, e quem quiser opinar usa a thread.

| Permissão do bot no canal | Para que serve |
| --- | --- |
| Ver Canal e Enviar Mensagens | Publicar o embed da sugestão |
| Inserir links (Embed Links) | Mostrar o embed |
| Criar Tópicos Públicos (Create Public Threads) | Abrir a thread na mensagem |
| Enviar Mensagens em Tópicos | Avisar na thread quando a sugestão for avaliada |

Agora a definição dos dois comandos. O primeiro é aberto a todos. O segundo, de avaliação, nasce restrito a quem tem a permissão Gerenciar Servidor. O registro dos comandos na API está no guia de [slash commands](https://streethosting.com.br/guias/bots-discord/criar-bot-discord-slash-commands).

```
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');

const sugerirCmd = new SlashCommandBuilder()
  .setName('sugerir')
  .setDescription('Envie uma sugestão para o servidor')
  .addStringOption((o) =>
    o.setName('texto').setDescription('Sua sugestão').setRequired(true).setMaxLength(1000)
  );

const avaliacao = (nome, descricao) => (sub) =>
  sub
    .setName(nome)
    .setDescription(descricao)
    .addIntegerOption((o) =>
      o.setName('numero').setDescription('Número da sugestão').setRequired(true).setMinValue(1)
    )
    .addStringOption((o) =>
      o.setName('motivo').setDescription('Motivo da decisão').setRequired(true).setMaxLength(500)
    );

const sugestaoCmd = new SlashCommandBuilder()
  .setName('sugestao')
  .setDescription('Avaliar sugestões (equipe)')
  .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
  .addSubcommand(avaliacao('aprovar', 'Aprovar uma sugestão'))
  .addSubcommand(avaliacao('recusar', 'Recusar uma sugestão'));
```

## O comando /sugerir

Primeiro a base: o cliente, o armazenamento em arquivo e a função que desenha os botões. Cada sugestão fica em um objeto com número, autor, texto, status e as duas listas de votantes. O arquivo `sugestoes.json` é regravado a cada mudança, o que sustenta o sistema depois de reinícios.

```
const fs = require('node:fs');
const {
  Client, GatewayIntentBits, Events, MessageFlags, EmbedBuilder, Colors,
  ActionRowBuilder, ButtonBuilder, ButtonStyle, PermissionFlagsBits,
} = require('discord.js');

const CANAL_ID = process.env.SUGESTOES_CANAL_ID;
const ARQUIVO = './sugestoes.json';

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

// itens é indexado pelo ID da mensagem da sugestão
let dados = { proximo: 1, itens: {} };
try {
  dados = JSON.parse(fs.readFileSync(ARQUIVO, 'utf8'));
} catch {
  // primeira execução, começa vazio
}
const salvar = () => fs.writeFileSync(ARQUIVO, JSON.stringify(dados));

function botoes(s, desativados = false) {
  return new ActionRowBuilder().addComponents(
    new ButtonBuilder()
      .setCustomId('voto:sim')
      .setLabel('Concordo (' + s.sim.length + ')')
      .setEmoji('👍')
      .setStyle(ButtonStyle.Success)
      .setDisabled(desativados),
    new ButtonBuilder()
      .setCustomId('voto:nao')
      .setLabel('Discordo (' + s.nao.length + ')')
      .setEmoji('👎')
      .setStyle(ButtonStyle.Danger)
      .setDisabled(desativados),
  );
}
```

E agora o tratamento do comando. Ele monta o embed, envia no canal de sugestões, grava os dados e abre a thread:

```
async function sugerir(interaction) {
  const canal = await interaction.client.channels.fetch(CANAL_ID).catch(() => null);
  if (!canal) {
    return interaction.reply({
      content: 'O canal de sugestões não está configurado.',
      flags: MessageFlags.Ephemeral,
    });
  }

  const texto = interaction.options.getString('texto');
  const numero = dados.proximo++;
  const s = {
    numero,
    autorId: interaction.user.id,
    status: 'aberta',
    sim: [],
    nao: [],
  };

  const embed = new EmbedBuilder()
    .setColor(Colors.Gold)
    .setAuthor({
      name: interaction.user.username,
      iconURL: interaction.user.displayAvatarURL(),
    })
    .setTitle('Sugestão #' + numero)
    .setDescription(texto)
    .setFooter({ text: 'Em votação' })
    .setTimestamp();

  const mensagem = await canal.send({ embeds: [embed], components: [botoes(s)] });

  s.mensagemId = mensagem.id;
  dados.itens[mensagem.id] = s;
  salvar();

  await mensagem
    .startThread({
      name: ('Sugestão #' + numero + ': ' + texto).replace(/\s+/g, ' ').slice(0, 100),
      autoArchiveDuration: 1440,
    })
    .catch((erro) => console.error('Não consegui abrir a thread:', erro.message));

  await interaction.reply({
    content: 'Sua sugestão foi enviada em ' + canal.toString() + '.',
    flags: MessageFlags.Ephemeral,
  });
}
```

A resposta ao autor é efêmera, ou seja, só ele vê, e isso evita poluir o canal onde o comando foi usado. O `autoArchiveDuration` de 1440 minutos arquiva a thread depois de um dia sem atividade. Os valores aceitos são 60, 1440, 4320 e 10080, e a thread reabre sozinha quando alguém escreve nela.

## Votos com botões e sem voto duplo

O truque contra voto duplo é guardar quem votou, e não só quantos votaram. Em cada clique o bot consulta as duas listas da sugestão e aplica a regra: no máximo um voto por pessoa, com troca de lado permitida e clique repetido retirando o voto.

```
async function votar(interaction) {
  const s = dados.itens[interaction.message.id];

  if (!s || s.status !== 'aberta') {
    return interaction.reply({
      content: 'Esta sugestão já foi encerrada.',
      flags: MessageFlags.Ephemeral,
    });
  }

  const id = interaction.user.id;
  if (id === s.autorId) {
    return interaction.reply({
      content: 'Você não pode votar na sua própria sugestão.',
      flags: MessageFlags.Ephemeral,
    });
  }

  const lado = interaction.customId === 'voto:sim' ? 'sim' : 'nao';
  const outro = lado === 'sim' ? 'nao' : 'sim';

  // tira o voto do lado oposto, se existir
  s[outro] = s[outro].filter((x) => x !== id);

  // clicar de novo no mesmo lado retira o voto
  if (s[lado].includes(id)) {
    s[lado] = s[lado].filter((x) => x !== id);
  } else {
    s[lado].push(id);
  }

  salvar();
  await interaction.update({ components: [botoes(s)] });
}
```

O `interaction.update` edita a mensagem em que o clique aconteceu, atualizando os números nos botões sem mandar mensagem nova. O bloqueio ao voto na própria sugestão é opcional, mas costuma valer a pena: sem ele o autor pode puxar a contagem a seu favor.

> **Atenção**
>
> Um voto por conta não impede quem cria contas extras. Se o seu servidor é alvo disso, exija um tempo mínimo de conta ou de permanência no servidor antes de liberar o voto. O Discord não oferece como garantir que cada humano tenha uma conta só.

## Aprovar ou recusar com motivo

A equipe usa `/sugestao aprovar` ou `/sugestao recusar` informando o número da sugestão e o motivo. O bot busca a mensagem, copia o embed com `EmbedBuilder.from`, troca a cor, acrescenta um campo com a resposta e desativa os botões.

```
async function avaliar(interaction) {
  // confirma a permissão no código, mesmo com setDefaultMemberPermissions
  if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
    return interaction.reply({
      content: 'Só a equipe pode avaliar sugestões.',
      flags: MessageFlags.Ephemeral,
    });
  }

  const aprovar = interaction.options.getSubcommand() === 'aprovar';
  const numero = interaction.options.getInteger('numero');
  const motivo = interaction.options.getString('motivo');

  const s = Object.values(dados.itens).find((x) => x.numero === numero);
  if (!s || s.status !== 'aberta') {
    return interaction.reply({
      content: 'Não achei uma sugestão aberta com o número #' + numero + '.',
      flags: MessageFlags.Ephemeral,
    });
  }

  const canal = await interaction.client.channels.fetch(CANAL_ID);
  const mensagem = await canal.messages.fetch(s.mensagemId).catch(() => null);
  if (!mensagem) {
    return interaction.reply({
      content: 'A mensagem dessa sugestão foi apagada.',
      flags: MessageFlags.Ephemeral,
    });
  }

  s.status = aprovar ? 'aprovada' : 'recusada';
  salvar();

  const embed = EmbedBuilder.from(mensagem.embeds[0])
    .setColor(aprovar ? Colors.Green : Colors.Red)
    .setFooter({ text: aprovar ? 'Aprovada' : 'Recusada' })
    .addFields({
      name: 'Resposta da equipe (' + interaction.user.username + ')',
      value: motivo,
    });

  await mensagem.edit({ embeds: [embed], components: [botoes(s, true)] });

  // a thread tem o mesmo ID da mensagem. Busca na API porque uma thread
  // arquivada sai do cache e mensagem.thread volta null
  const thread = await canal.threads.fetch(mensagem.id).catch(() => null);
  if (thread) {
    await thread
      .send('Sugestão ' + (aprovar ? 'aprovada' : 'recusada') + ': ' + motivo)
      .catch(() => {});
  }

  await interaction.reply({
    content: 'Sugestão #' + numero + (aprovar ? ' aprovada.' : ' recusada.'),
    flags: MessageFlags.Ephemeral,
  });
}
```

Por fim, o roteador que liga tudo ao evento `interactionCreate`. Os botões são reconhecidos pelo prefixo do `customId`:

```
client.on(Events.InteractionCreate, async (interaction) => {
  try {
    if (interaction.isChatInputCommand()) {
      if (interaction.commandName === 'sugerir') await sugerir(interaction);
      if (interaction.commandName === 'sugestao') await avaliar(interaction);
    } else if (interaction.isButton() && interaction.customId.startsWith('voto:')) {
      await votar(interaction);
    }
  } catch (erro) {
    console.error('Erro no sistema de sugestões:', erro);
  }
});

client.login(process.env.DISCORD_TOKEN);
```

> **Dica**
>
> Para a equipe, a forma mais rápida de achar o número é olhar o título do embed. Se preferir um fluxo com formulário, o guia de [modals e formulários](https://streethosting.com.br/guias/bots-discord/bot-discord-modals-formularios) mostra como abrir uma janela de texto no lugar da opção do comando.

## Alternativa: votar com reações

Dá para trocar os botões por reações, que muita comunidade prefere pelo visual. O bot envia o embed e reage com 👍 e 👎 para os membros clicarem. O custo é perder o controle fino: a contagem do Discord inclui o voto do próprio bot, que precisa ser descontado, e cada pessoa pode reagir aos dois emojis. Para impedir isso, remova a reação oposta por código. Você precisa da intent `GuildMessageReactions`, dos partials de mensagem, canal e reação (para receber eventos de mensagens antigas) e da permissão Gerenciar Mensagens para o bot:

```
const OPOSTA = { '👍': '👎', '👎': '👍' };

client.on(Events.MessageReactionAdd, async (reacao, usuario) => {
  if (usuario.bot) return;
  if (reacao.partial) await reacao.fetch().catch(() => null);
  if (reacao.message.channelId !== CANAL_ID) return;
  // mensagem antiga: busca para ter as outras reações no cache
  if (reacao.message.partial) await reacao.message.fetch().catch(() => null);

  const oposta = OPOSTA[reacao.emoji.name];
  if (!oposta) return;

  const outra = reacao.message.reactions.cache.get(oposta);
  if (outra) await outra.users.remove(usuario.id).catch(() => {});
});
```

Mesmo assim, para um canal de sugestões que precisa de números confiáveis, os botões são a escolha mais segura. Se a ideia é só uma votação rápida de sim ou não, sem fluxo de aprovação, veja o guia de [enquetes e votação](https://streethosting.com.br/guias/bots-discord/bot-discord-enquetes-votacao).

## Cuidados para não perder dados

- **Não use collectors para os botões.** Um collector só existe enquanto o processo está vivo. Tratando o clique direto no `interactionCreate`, os botões de sugestões antigas continuam funcionando depois de um deploy ou queda.
- **Persista tudo que importa.** O arquivo JSON basta para um servidor pequeno. Com muitas sugestões ou mais de um servidor, use um banco de dados. O plano de bots inclui um banco MariaDB, e se preferir MongoDB, o guia de [conectar o bot a um banco MongoDB](https://streethosting.com.br/guias/bots-discord/conectar-bot-discord-banco-mongodb) mostra o caminho.
- **Faça cópia do arquivo.** Sem backup, um disco com problema leva todo o histórico de votos junto.
- **Limite o abuso.** Um intervalo mínimo entre sugestões do mesmo usuário evita spam no canal, e o limite de 1000 caracteres no comando mantém tudo legível.

## Onde hospedar o bot

Um sistema de sugestões é leve, mas só funciona com o bot online: com ele fora do ar, ninguém consegue votar nem enviar ideias. Na [hospedagem de bots da StreetHosting](https://streethosting.com.br/bots) o plano Discord Essential, a R$ 9,99 por mês, com 1 GB de RAM, 0,5 vCPU, 2 GB de NVMe e até 2 bots, roda esse projeto em Node.js com folga, tudo controlado pelo painel do container. Para manter o processo de pé o tempo todo, veja [hospedagem de bot Discord 24/7](https://streethosting.com.br/guias/bots-discord/hospedagem-bot-discord-24-7).

## Perguntas frequentes

### Como impedir que a mesma pessoa vote duas vezes?

Guarde, para cada sugestão, a lista dos IDs de quem votou em cada lado. Ao receber um clique, remova o ID do lado oposto, e se ele já estava no lado clicado, trate como retirada do voto. Assim cada pessoa tem no máximo um voto e ainda pode mudar de ideia.

### Os botões continuam funcionando depois que o bot reinicia?

Continuam, desde que você trate o clique no evento interactionCreate usando o customId e o ID da mensagem, em vez de usar um collector. O collector vive na memória e morre com o processo. E os dados dos votos precisam estar em arquivo ou banco, não só em variável.

### Quem pode aprovar ou recusar sugestões?

Quem você definir. Os dois caminhos comuns são exigir a permissão Gerenciar Servidor, com setDefaultMemberPermissions no comando e uma checagem dentro do código, ou conferir se a pessoa tem um cargo de equipe cujo ID fica na configuração do bot.

### Votar com botões ou com reações: qual é melhor?

Botões dão contagem exata, impedem voto duplo de forma confiável e podem ser desativados ao encerrar. Reações são mais simples e visuais, mas contam o próprio voto do bot e permitem votar nos dois lados, a menos que você remova a reação oposta por código.

### Qual o tamanho máximo da sugestão?

A descrição de um embed aceita até 4096 caracteres, mas para sugestões vale limitar a 1000 com setMaxLength na opção do comando, o que mantém o canal legível e evita textos gigantes.

## Guias relacionados

- [Enquete no Discord com bot: enquete nativa, reações ou botões](https://streethosting.com.br/guias/bots-discord/bot-discord-enquetes-votacao.md)
- [Botões e menus de seleção no bot Discord com discord.js v14](https://streethosting.com.br/guias/bots-discord/bot-discord-botoes-menus-interativos.md)
- [Embeds no Discord com discord.js: como montar mensagens formatadas](https://streethosting.com.br/guias/bots-discord/bot-discord-embeds-mensagens-formatadas.md)
- [Modals e formulários no bot Discord: candidatura para staff com discord.js](https://streethosting.com.br/guias/bots-discord/bot-discord-modals-formularios.md)
- [Bot Discord com sistema de tickets para suporte](https://streethosting.com.br/guias/bots-discord/bot-discord-sistema-tickets-suporte.md)

## Produtos citados

- https://streethosting.com.br/bots

## Dados estruturados

```json
[
  {
    "@context": "https://schema.org",
    "@type": [
      "Article",
      "TechArticle"
    ],
    "headline": "Sistema de sugestões no Discord: /sugerir, votos, aprovação da equipe e thread",
    "name": "Sistema de sugestões no Discord com votação, aprovação e thread",
    "abstract": "Um canal de sugestões organizado: a pessoa usa /sugerir, o bot posta um embed com botões de voto, abre uma thread para a conversa e a equipe aprova ou recusa com motivo, trocando a cor do embed.",
    "description": "Monte um sistema de sugestões com discord.js v14: /sugerir, votos por botões sem voto duplo, aprovação com motivo, embed que muda de cor e thread de debate.",
    "datePublished": "2026-11-13",
    "dateModified": "2026-11-13",
    "author": {
      "@type": "Organization",
      "name": "Equipe StreetHosting"
    },
    "publisher": {
      "@type": "Organization",
      "name": "StreetHosting",
      "url": "https://streethosting.com.br"
    },
    "inLanguage": "pt-BR",
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": "https://streethosting.com.br/guias/bots-discord/bot-discord-sistema-sugestoes"
    }
  },
  {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {
        "@type": "Question",
        "name": "Como impedir que a mesma pessoa vote duas vezes?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Guarde, para cada sugestão, a lista dos IDs de quem votou em cada lado. Ao receber um clique, remova o ID do lado oposto, e se ele já estava no lado clicado, trate como retirada do voto. Assim cada pessoa tem no máximo um voto e ainda pode mudar de ideia."
        }
      },
      {
        "@type": "Question",
        "name": "Os botões continuam funcionando depois que o bot reinicia?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Continuam, desde que você trate o clique no evento interactionCreate usando o customId e o ID da mensagem, em vez de usar um collector. O collector vive na memória e morre com o processo. E os dados dos votos precisam estar em arquivo ou banco, não só em variável."
        }
      },
      {
        "@type": "Question",
        "name": "Quem pode aprovar ou recusar sugestões?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Quem você definir. Os dois caminhos comuns são exigir a permissão Gerenciar Servidor, com setDefaultMemberPermissions no comando e uma checagem dentro do código, ou conferir se a pessoa tem um cargo de equipe cujo ID fica na configuração do bot."
        }
      },
      {
        "@type": "Question",
        "name": "Votar com botões ou com reações: qual é melhor?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Botões dão contagem exata, impedem voto duplo de forma confiável e podem ser desativados ao encerrar. Reações são mais simples e visuais, mas contam o próprio voto do bot e permitem votar nos dois lados, a menos que você remova a reação oposta por código."
        }
      },
      {
        "@type": "Question",
        "name": "Qual o tamanho máximo da sugestão?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "A descrição de um embed aceita até 4096 caracteres, mas para sugestões vale limitar a 1000 com setMaxLength na opção do comando, o que mantém o canal legível e evita textos gigantes."
        }
      }
    ]
  }
]
```
