DEV Community

Stefan Bohacek for Botwiki

Posted on

Making a trivia quiz chatbot on Mastodon

A screenshot of the bot posting a country flag and me responding with the correct answer, followed by the bot acknowledging the answer and posting the leaderboard

TLDR: Here's the finished code | Get in touch if you get stuck


I made the original @what_capital bot, which lets you play a trivia game where you identify capitals based on a country's flag, about seven years ago, for a tutorial that you'd no longer be able to follow. For one, the web-based IDE I used was sold to Amazon, and I'm not sure you can easily use it for free. Second, the hosting service I used seem to no longer offer a free plan. And finally, Twitter, whose API I used, is in disarray.

Yes, seven years is a really long time on the web.

But as some technologies and platforms disappear, new ones arise. And one that I am particularly excited about is the fediverse. It's an open standard for making a better social web with roots going back to 2008. And the way many folks are being introduced to it is via the rise of Mastodon, both a social media site, and software to make your own version, that's built on this standard.

And a fun way to explore the fediverse is using Mastodon's API to re-create my bot.

And for that, I am going to use my Creative Bots project. You have a few options how to host it, but for this tutorial I am going to go with Glitch for its ease of use. Note that you don't need an account to follow along, but you will need a paid plan to keep your bot running after we're done.

First, you will need to set up an account for your bot. Any Mastodon instance will do. Note that the popular botsin.space instance has a manual review process in place, so you might have to wait a bit if you decide to go with that one.

Once we have our account ready, let's make a remix of the Creative Bots Glitch app.

Next, we'll update our .env file (see the sidebar on the left) and add the following variables.

WHAT_CAPITAL_BOT_MASTODON_API_URL="https://botsin.space/api/v1/"
WHAT_CAPITAL_BOT_MASTODON_ACCESS_TOKEN="123456789-abcd"
Enter fullscreen mode Exit fullscreen mode

Screenshot from glitch.com showing how to add environmental variables

Replace botsin.space with the name of your bot's instance, if you're not using that one. For example, if your bot is on mastodon.social, you will use:

WHAT_CAPITAL_BOT_MASTODON_API_URL="https://mastodon.social/api/v1/"
WHAT_CAPITAL_BOT_MASTODON_ACCESS_TOKEN="123456789-abcd"
Enter fullscreen mode Exit fullscreen mode

So far, so good.

Now, back in the sidebar, open bots/reply-mastodon.js. This is a simple example of a bot that replies to messages sent to it, and we will expand on it to make our bot.

But before we write any code, we will need to prepare some data. Using the sidebar again, add a file called data/capitals.js.

A screenshot showing how to add a new file in Glitch.

You can get the contents from the finished project here.

Note that the JSON object with answers also contains description of each flag. More on that in a bit.

Now, let's update your bot's code to use the correct access token and API URL.

const mastodon = new mastodonClient({
   access_token: process.env.WHAT_CAPITAL_BOT_MASTODON_ACCESS_TOKEN,
   api_url: process.env.WHAT_CAPITAL_BOT_MASTODON_API_URL
});
Enter fullscreen mode Exit fullscreen mode

At this point, you can already try speaking with your bot. Go ahead and say hello!

After you have a bit of fun with your new bot, let's get back to writing some more code. All that's next will come before the module.exports section.

Here's the finished code again so that you can check your progress.

To start, we will need to load our capitals.js file, so let's do just that.

const fs = require("fs"),
      helpers = require(__dirname + "/../helpers/helpers.js"),
      capitals = require(__dirname + "/../data/capitals.js"),
      mastodonClient = require(__dirname + "/../helpers/mastodon.js");
Enter fullscreen mode Exit fullscreen mode

Note that I also added the fs module. This will be useful to keep track of a leaderboard, to make the game a bit competitive.

Note that the .data folder, which we will use, is a built-in way for Glitch to store data for your app.

const savedDataPath = __dirname + "/../.data/what_capital.json";

let savedData = {
  country: "",
  capital: "",
  scores: {},
};

if (fs.existsSync(savedDataPath)) {
  savedData = JSON.parse(fs.readFileSync(savedDataPath, "utf8"));
}

const saveData = () => {
  fs.writeFileSync(savedDataPath, JSON.stringify(savedData, null, 2), "utf8");
};

const updateScores = (user) => {
  if (savedData.scores.hasOwnProperty(user)) {
    savedData.scores[user] = savedData.scores[user] + 1;
  } else {
    savedData.scores[user] = 1;
  }
  saveData();
};

Enter fullscreen mode Exit fullscreen mode

Great. Now we'll need a pair of functions that will handle picking a new question and verifying the answer.

const pickNewCapital = () => {
  const capital = helpers.randomFromArray(capitals);
  const flagUrl = `https://static.stefanbohacek.dev/images/flags/${capital.country.replace(
    / /g,
    "_"
  )}.png`;

  savedData.capital = capital.capital;
  savedData.country = capital.country;
  saveData();

  helpers.loadImage(flagUrl, (err, imgData) => {
    if (err) {
      console.log(err);
    } else {
      let altText = capital.flag_description;

      if (capital.flag_description.length > 1000) {
        altText = capital.flag_description.slice(0, 2) + "...";
      }

      mastodon.postImage({
        status: "What is the capital of this country?",
        image: imgData,
        alt_text: `An unspecified country flag: ${altText}`,
      });
    }
  });
};

const checkAnswer = (answer) => {
  answer = answer
    .trim()
    .toLowerCase()
    .normalize("NFD")
    .replace(/\p{Diacritic}/gu, "");
  const correctAnswer = savedData.capital
    .toLowerCase()
    .normalize("NFD")
    .replace(/\p{Diacritic}/gu, "");
  return answer.includes(correctAnswer);
};
Enter fullscreen mode Exit fullscreen mode

This is the really tricky part. People will likely include words that are not part of the answer in their message, like "Hmm", or "I think...". And some city names may contain special characters. To make a bot that's fun to interact with, we need to account for all the possible ways people will talk to it.

We could actually go a step further here, and use an array with acceptable variations of each city's name, maybe even the translation in its native language. There's an idea for a future update.

And going back to the code above, you will notice that I am using the flag description as an alt text for the image. This way your bot can be enjoyed by everyone.

let altText = capital.flag_description;
Enter fullscreen mode Exit fullscreen mode

Now we're ready to bring this all together inside the reply function of our bot object.

let reply = "";

if (
  fullMessage.data.status.visibility === "public" ||
  fullMessage.data.status.visibility === "unlisted"
) {
  if (checkAnswer(messageText)) {
    updateScores(from);
    reply = `Yes, ${savedData.capital} is the capital of ${
      savedData.country
    }, correct! ${getLeaderboard()}`;
    pickNewCapital();
  } else {
    reply = "That doesn't seem correct, sorry! Or perhaps a new flag was posted?";
  }
} else {
  reply = "Sorry, do you mind responding publicly?";
}

mastodon.reply(fullMessage, reply);
Enter fullscreen mode Exit fullscreen mode

And there you have it, your very own interactive game bot, running on the open web.

Feel free to browse more examples of bots I've made using my starter project, and you can find even more resources for creative botmakers over at botwiki.org, and join the botmakers.org Slack group.

I hope you find this tutorial fun. If you have any questions or suggestions, feel free to reach out.

Until next time!

Top comments (0)