Building an Unsplash chatbot for Discord
Tags: chatbot, discord, javascript, api
Published: 2022-01-27 14:36:28

Discord is a popular realtime chat application that has gamer-centric roots but has recently repositioned itself towards online communities in general. Unsplash has long become a de-facto source of gorgeous free to use pictures, powered by its amazing community of creators. It is only natural to marry the two and in this two posts series I'll show you how to build a chatbot that brings pictures from Unsplash to Discord chats in a couple of different ways.

## Tools
In order to build the chatbot I'll obviously utilize [Unsplash API](https://unsplash.com/documentation) which is really simple and easy to use.

For the chatbot logic I will use [Just AI Conversational Platform (JAICP)](https://jaicp.com/en/?utm_source=dev.to&utm_medium=referral&utm_campaign=discord-unsplash), which is an enterprise-grade platform that enables customers to design, develop, deploy and operate intelligent conversational AI assistants in a wide variety of text and voice channels (**disclaimer**: I work there). One benefit of using such platform is if you build a Discord bot and then decide to use it in Facebook Messenger, you can do that in a few clicks without changes to your code. We provide a pretty generous free tier, which means you could run a bot for your community for free or at a low fee.

Finally, because JAICP doesn't have a native Discord integration (yet) but has an API that allows for 3rd party integrations, I will build a Node.js adapter that translate messages from Discord to JAICP and vice versa.

## Discord bot
The first step is to step a Discord application for your bot through the [Developer Portal](https://discord.com/developers/applications).

On the Applications page click **New Application** button in the top navigation bar. In the popup window enter something like "jaicp-discord-unsplash" and then click **Create**. You will be redirected to your application's page where you need to select **Bot** tab on the left hand side. Click **Add Bot** and then **Yes, do it!** - this will turn your app into a bot and provide you with settings, specifically bot token that we will use later on.

![Discord Developer Portal - Application bot settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t17z2rbxsvr5tx9i8t9j.png)

> Remember to keep your bot token **private** and immediately regenerate it if you suspect it's been compromised as it gives the full access to your bot actions.

## Just AI Conversational Platform (JAICP)
Next, we need to create a JAICP account. After you signed up using a method of your choosing, click **Create Project** button on the left hand side, choose **Create from scratch** option in the wizard, and finally specify a name, like "jaicp-discord-unsplash". For the purpose of this article I'll keep my bot's code in the built-in repository but alternatively you can choose to use an external Git provider like Github.

![JAICP - Project creation flow screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t1bdqybjphixbt15mlc7.png)

### Looking around
You will not actually start from scratch but a Hello World example that will show a bit of a [powerful JAICP DSL](https://help.just-ai.com/docs/en/JAICP_DSL/about_dsl?utm_source=dev.to&utm_medium=referral&utm_campaign=discord-unsplash) which is based on YAML and Javascript and provides many useful abstractions that allow you to build complex chatbots and voice assistants across different channels very quickly and efficiently.

The main scenario file is `main.sc` where we'll basically define bot's state machine (finite-state machine, FST), transitions between states in response to user's input and responses that the bot provides back to users.

Now let's create a bot that will activate on phrases like _search Unsplash_ or _find images on Unsplash_, ask a user what they'd like to search for, perform an API request and finally return results.

### Unsplash part

But before we develop any scenario code, let's write a simple Javascript client for Unsplash API.

```javascript
var UnsplashAPI = {
    search: function (query) {
        var pictures = $http.get("https://api.unsplash.com/search/photos?page=1&per_page=3&query=" + query, {
            dataType: "json",
            query: {
                query: query
            },
            headers: {
                "Authorization": "Client-ID // replace with your Unsplash API key"
            },
            timeout: 10000
        });
        return pictures.data;
    }
}
```
This code defines a very simple function that performs a call to Unsplash API and returns the first 3 pictures that match a query in a JSON format. It uses JAICP's built-in `$http` service that allows you to integrate bots with external systems.

### Intent recognition

Next, let's setup intent recognition for the bot, even if there's only one intent for now. Go to the **Intents** page under **CAILA** (Conversational AI Linguist Assistant, which is JAICP's NLU component) folder on the left hand side and click **Create Intent** at the top. 

Name the intent `search` and under training phrases enter some that you would normally associate with searching for images on the internet - some examples include _search unsplash_, _find images_. Then you can test the model right there and verify that `search` intent is recognized correctly.
![Intent recognition setup interface in Just AI Conversation Platform (JAICP)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/klj0l532gkg6ulr9rm5x.png)

### Scenario

Now that API and intents are sorted out, let's turn our attention to the bot scenario. Go back to `main.sc` and append the following code into it:

``` javascript
state: SearchUnsplash
    intent: /search
    go!: /SearchUnsplash/RequestQuery

    state: RequestQuery
        a: What should I search for?
    
        state: SearchPictures
            q: *
            script:
                var query = $request.query;
                var pictures = UnsplashAPI.search(query);
    
                $response.replies = $response.replies || [];
                var content = [];
                pictures.results.forEach(function (picture) {
                    content.push({
                        "title": picture.description,
                        "image": picture.urls.small,
                        "url": picture.links.html,
                        "btnText": "View on Unsplash"
                    });
                });
                
                var reply = {
                    "type": "carousel",
                    "text": "Unsplash search results for \"" + query + "\":",
                    "content": content
                };
                $response.replies.push(reply);
            go: /
```
There are a few things going on here. First, **SearchUnsplash** state of the state machine is defined, which is soft of an activation state for our bot. The bot will enter into it every time `search` intent is recognized from user's input. All it does is to redirect to the next (nested) state, **/SearchUnsplash/RequestQuery**, which will ask a user what they want for search for. Nesting here means, that once the root intent is recognized, subsequent user input will be processed (intent recognition, slot filling) in the context of that root intent.

Once a user enters a search query it will put the chatbot into **SearchPictures** state because it has a wildcard (*) matching - indeed, search query is an arbitrary sentence.

Finally we have our search query and can fulfill user's intent by putting a few lines of Javascript in the `script` block. Here we call an Unsplash API wrapper that we created earlier and transform results into a reply message of type [Carousel](https://help.just-ai.com/docs/en/bot_answers/reply_types?utm_source=dev.to&utm_medium=referral&utm_campaign=discord-unsplash#carousel), which is tailor-made for outputting lists of data, even though the end look-and-feel may vary based on a channel, whether it's Discord or Facebook or something else.

### Setting up Chat API
The last thing we need to do in JAICP is to set up Chat API credentials for us to be able to communicate with external chat platform such as a Discord.

Go to **Channels**, click _Connect channel_ under the **Inbound** section and select Chat API. Once you hit **Save**, you will be able to grab access token which is requires to the final step.

![Setting up Chat API in JAICP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vpdc6eli3jd4n5i2kaxg.png)

## Putting it all together
One key component is still missing - as I just mentioned, JAICP currently doesn't a native Discord integration, but does have an extension point called [Chat API](https://help.just-ai.com/docs/en/api/chat_api?utm_source=dev.to&utm_medium=referral&utm_campaign=discord-unsplash), which allows developers to integrate conversational AI solution into a pretty much any chat platform.

I've created a really simple adapter in Node.js that listens for messages on Discord, interacts with JAICP via Chat API and then replies back to Discord. It makes use of an excellent [Discord.js](https://discord.js.org) library and also [axios](https://axios-http.com) for http requests.

```javascript
const client = new Client({
    intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES],
  });
client.on("message", function (message) {
  if (message.author.bot) return;

  message.channel.sendTyping();

  axios
    .post(
      `https://app.jaicp.com/chatapi/${process.env.JAICP_CHAT_API_KEY}`,
      {
        query: message.content,
        clientId: message.author.id,
      },
      {
        headers: {
          "Content-Type": "application/json",
        },
      }
    )
    .then(function (response) {
      response.data.data.replies.forEach(async (reply) => {
        if (reply.type === "text") {
          message.channel.send(reply.text);
        } else if (reply.type === "carousel") {
          message.channel.send(reply.text);

          reply.content.forEach((item) => {
            let embed = new MessageEmbed()
              .setImage(item.image)
              .setDescription(item.title || "No description")
              .setURL(item.url);

            let actionRow = new MessageActionRow();
            let b = new MessageButton()
              .setLabel(item.btnText)
              .setStyle(5)
              .setURL(item.url);
            actionRow.addComponents(b);

            message.channel.send({ embeds: [embed], components: [actionRow] });
          });
        }
      });
    })
    .catch(function (error) {
      console.log(error);
    });
});
client.login(process.env.BOT_TOKEN);
```
We instantiate a websocket client that listens for Discord messages to come in, passes the text content to JAICP and transforms the replies back into Discord format. In particular, it uses embeds for images themselves and action rows/buttons to provide a link back to Unsplash - it's a nice thing to do even if Unsplash doesn't require attribution. Although JAICP can return many more than just two reply types, `text` and `carousel` are the only two we need to handle for this tutorial.

For the purpose of this article I just run it locally but you can obviously deploy it to something like Heroku.

Once you run it and try the bot in Discord it should look something like this:

![Discord bot showing pictures from Unsplash tagged "coffeeshop"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wygkmaktv9jm01onv6g0.png)

You can see that I asked the bot to search for pictures of coffeeshops, and indeed I got what I wanted.

## Conclusion

In this tutorial we saw how to add a conversational bot to Discord that can potentially do much more than just post pictures, even though pictures from Unsplash are typically gorgeous. This is actually a part 1 of the two piece series - in part 2 I'll show you how to set up a scheduled _photo of the day_ post.

You can find the code both of the [chatbot project](https://github.com/hiisi13/jaicp-discord-unsplash-bot) and [Discord adapter](https://github.com/hiisi13/jaicp-discord-adapter) on Github.

Naturally, the cover photo of this post is from Unsplash Photo by [Chuck Fortner](https://unsplash.com/@chuckfortner?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText).