Over the last two weeks, we’ve explored the various ways in which Discord bots can process messages. In this article, we’ll be putting all that together to create a bot that will gather input from a user to create a survey to gather feedback from other users. Strap in, this is gonna be a long one!

Using Collectors to Build the Survey

To start, lets add a !survey command to start. We wont require any additional arguments here, we’ll jump right into prompting the user for the following info;

  • What the survey is answering.
  • How long it should run.
  • A series of potential answers.

Add the following code at the end of your message event handler;

if (msg.content.startsWith('!survey')) {
    // Create an empty 'survey' object to hold the fields for our survey
    let survey = {};
    // We're going to use this as an index for the reactions being used.
    let reactions = [
      "1️⃣",
      "2️⃣",
      "3️⃣",
      "4️⃣"
    ]
    // Send a message to the channel to start gathering the required info
    msg.channel
      .send(
        'Welcome to the !survey command. What would you like to ask the community?'
      )
      .then(() => {
        // After each question, we setup a collector just like we did previously
        let filter = (msg) => !msg.author.bot;
        let options = {
          max: 1,
          time: 15000
        };

        return msg.channel.awaitMessages(filter, options);
      })
      .then((collected) => {
        // Lets take the input from the user and store it in our 'survey' object
        survey.question = collected.array()[0].content;
        // Ask the next question
        return msg.channel.send(
          'Great! How long should it go? (specified in seconds)'
        );
      })
      .then(() => {
        let filter = (msg) => !msg.author.bot;
        let options = {
          max: 1,
          time: 15000
        };

        return msg.channel.awaitMessages(filter, options);
      })
      .then((collected) => {
        // Adding some checks here to ensure the user entered a number.
        if (!isNaN(collected.array()[0].content)) {
          survey.timeout = collected.array()[0].content;
          // Ask the final question
          return msg.channel.send(
            'Excellent. Now enter up to four options, separated by commas.'
          );
        } else {
          throw 'timeout_format_error';
        }
      })
      .then(() => {
        let filter = (msg) => !msg.author.bot;
        let options = {
          max: 1,
          time: 15000
        };

        return msg.channel.awaitMessages(filter, options);
      })
      .then((collected) => {
        // Split the answers by commas so we have an array to work with
        survey.answers = collected.array()[0].content.split(',');
        console.log(survey)
      })
  }

You’ll see that after each message, we’re asking another another question and guiding the user down the path of setting up the information we need. To accomplish this, we can use the awaitMessages version of the collector which will return a Promise like we did in the previous article. This way we don’t need to nest all these into various event callbacks. It makes the code much cleaner and more understandable.

Here is the code in action;

And the output in our terminal;

Creating the Reaction Collector

Now that we’ve gathered the requirements for the survey, we need a way to display it for the server members to vote on. To do this, I’m going to use an Embed which is a rich card that can be used with Discord bots. Embeds are visually different than messages and provide additional formatting options. Lets get rid of that console.log() line in the last .then() of the promise chain and update it so it looks like this;

.then((collected) => {
  // Split the answers by commas so we have an array to work with
  survey.answers = collected.array()[0].content.split(',');

  let surveyDescription = ""
  // Loop through the questions and create the 'description' for the embed
  survey.answers.forEach((question, index) => {
    surveyDescription += `${reactions[index]}: ${question}\n`;
  })

  // Create the embed object and send it to the channel
  let surveyEmbed = new Discord.MessageEmbed()
    .setTitle(`Survey: ${survey.question}`)
    .setDescription(surveyDescription)
  return msg.channel.send(surveyEmbed)
})

Then add the following block to your promise chain;

.then(surveyEmbedMessage => {
  // Create the initial reactions to embed for the members to see
  for (var i = 0; i < survey.answers.length; i++) {
    surveyEmbedMessage.react(reactions[i])
  }

  // Set a filter to ONLY grab those reactions & discard the reactions from the bot
  const filter = (reaction, user) => {
    return reactions.includes(reaction.emoji.name) && !user.bot;
  };

  // Use the timeout from our survey
  const options = {
    time: survey.timeout * 1000
  }

  // Create the collector
  return surveyEmbedMessage.awaitReactions(filter, options);
})

Lets test our survey command one more time and see what the output looks like;

This an example of an embed. See how it sticks out from a standard message? We’ll get more into embeds when we explore webhooks in Discord next week.

Displaying the Results

Finally, we want to show the results of our survey. To do that, we’re basically going to get the collected reactions, reduce them down into another embed, and send it to the server to show the total vote count and the number of votes on each option. Add this final then() to the promise chain;

 .then(collected => {
    // Convert the collection to an array
    let collectedArray = collected.array()
    // Map the collection down to ONLY get the emoji names of the reactions
    let collectedReactions = collectedArray.map(item => item._emoji.name)
    let reactionCounts = {}

    // Loop through the reactions and build an object that contains the counts for each reaction
    // It will look something like this:
    // {
    //   1️⃣: 1
    //   2️⃣: 0
    //   3️⃣: 3
    //   4️⃣: 10
    // }
    collectedReactions.forEach(reaction => {
      if (reactionCounts[reaction]) {
        reactionCounts[reaction]++
      } else {
        reactionCounts[reaction] = 1
      }
    })

    // Using those results, rebuild the description from earlier with the vote counts
    let surveyResults = ""
    survey.answers.forEach((question, index) => {
      let voteCount = 0
      if (reactionCounts[reactions[index]]) {
        voteCount = reactionCounts[reactions[index]]
      }
      let voteCountContent = `(${voteCount} vote${voteCount !== 1 ? 's' : ''})`
      surveyResults += `${reactions[index]}: ${question} ${voteCountContent}\n`;
    })

    // Create the embed and send it to the channel
    let surveyResultsEmbed = new Discord.MessageEmbed()
      .setTitle(`Results for '${survey.question}' (${collectedArray.length} total votes)`)
      .setDescription(surveyResults)

    msg.channel.send(surveyResultsEmbed);
  })

And here is what the results look like;

Until now, most of our activity has been within Discord itself. Next week, we’ll explore how to interact with systems outside of Discord for some really cool integrations!