Unsplash chatbot for Discord, Pt. 2: more ways to bring pictures to Discord

hiisi13

Dmitry Kozhedubov

Posted on February 4, 2022

Unsplash chatbot for Discord, Pt. 2: more ways to bring pictures to Discord

In the previous post we've built a very basic Discord bot that can search Unsplash images for user specified query and output results to back a channel. In this article we'll expand on that functionality and allow Discord users to schedule a random picture for a later time.

Just AI Conversational Platform (JAICP)

The JAICP part of our bot needs two changes: implement /random endpoint of the Unsplash API and add two new states to the bot itself - one to process user's request to schedule a picture for later and another to fulfill it.

Unsplash API client

JS client is located in the script/functions.js file and here's the code that implements the /random endpoint:

var UnsplashAPI = {
    // ...

    random: function () {
        var picture = $http.get("https://api.unsplash.com/photos/random", {
            dataType: "json",
            headers: {
                "Authorization": "Client-ID //replace with your access key"
            }
        });
        return picture.data;    
    }
};
Enter fullscreen mode Exit fullscreen mode

This is simpler than search because it doesn't require any request parameters and returns only one image object.

Scheduling intent

Now we need to define an intent to schedule a picture for later and a slot with time for when to post. Go to CAILA -> Intents and create a new intent, I called it RandomPicForLater. Unlike our previous intent, Search, this one will have a slot.

Slots are similar to query parameters in HTTP GET request and slot filling is a task that conversational systems perform to gather slot values from a user.

Our RandomPicForLater intent will have one slot called reminderTime and will be of type @duckling.time. Duckling is a library that extracts entities from text, and it is one of the tools used in JAICP for this purpose. Entity types in Duckling are called dimensions and there's a number of them built in, among them is Time which suits us perfectly since we need to ask users when they want us to schedule a post for and then parse a text input into a datetime object.

User's intent might be expressed as "schedule unsplash pic" or "schedule a random picture". In return, we might ask something like "When do you want me to schedule it?" to get the posting time. Fill these values in the corresponding fields ⬇️

Editing Intent in Just AI Conversational Platform (JAICP)

Fulfillment

Back to the editor, add the following code to main.sc:

    ...
    state: ScheduleRandom
        intent!: /RandomPicForLater
        script:
            $session.reminderTime = $parseTree["_reminderTime"];
            var event = $pushgate.createEvent(
                $session.reminderTime.value,
                "scheduleEvent"
            );
            $session.reminderId = event.id;
            $temp.reminderTime = moment($session.reminderTime.value).locale("en").calendar();
        a: Very well, your random Unsplash picture will arrive at {{$temp.reminderTime}}.
Enter fullscreen mode Exit fullscreen mode

This a new state ScheduleRandom which is triggered by RandomPicForLater intent.

What happens in the script block is interesting, because we first retrieve that reminderTime slot value and then make use of JAICP's Pushgate API that allows you to create outbound communications, as in define custom events, handle them, send outbound messages and even have bots notify your systems via webhooks. Here specifically we schedule a new scheduleEvent at user requested time and then handle it in the next state ⬇️

    state: Remind
        event!: scheduleEvent
        script:
            var picture = UnsplashAPI.random();

            $response.replies = $response.replies || [];
            var content = [];
            log("picture_desc= " + picture.urls.description);
            log("picture_url= " + picture.urls.small);
            content.push({
                "title": picture.description || "No description",
                "image": picture.urls.small,
                "url": picture.links.html,
                "btnText": "View on Unsplash"
            });

            var reply = {
                "type": "carousel",
                "text": "Your scheduled random picture",
                "content": content
            };
            $response.replies.push(reply);
Enter fullscreen mode Exit fullscreen mode

Notice that Remind state is triggered not by an intent or a pattern match, but by scheduleEvent. The handler then does two things:

  • get a random picture from Unsplash API client
  • build a reply of type carousel, similar to what we did in Part 1, but with just a single item

The chatbot is now fully functional which you can verify by trying it in the test widget:
Testing Unsplash chatbot in JAICP test widget

Extending Discord adapter

The only problem now is that Discord adapter to Chat API only works in request-response fashion and doesn't actively listen for messages incoming for the chatbot server. Let's fix that.

JAICP's Chat API provides an events endpoint which clients can use to fetch server-initiated events. Every time a new Discord user starts a conversation with the bot, we'll start a very minimalistic loop that will periodically try to fetch server events that occurred after the last known response ⬇️

const startPollingLoopForUser = async function (userId, channel) {
  setInterval(async () => {
    const endpoint = `https://app.jaicp.com/chatapi/${process.env.JAICP_CHAT_API_KEY}/events`;
    const eventsResponse = await axios.get(endpoint, {
      params: {
        clientId: userId,
        ts: lastJaicpMessageTimestamps[userId],
      },
    });
    eventsResponse.data.events.forEach((event) => {
      if (event.type === "botResponse") {
        const ts = Date.parse(event.event.timestamp);
        if (
          event.event.questionId !== lastQuestionIds[userId] &&
          ts > lastJaicpMessageTimestamps[userId]
        ) {
          lastJaicpMessageTimestamps[userId] = ts;
          lastQuestionIds[userId] = event.event.questionId;
          event.event.data.replies.forEach((reply) => {
            processReply(channel, reply);
          });
        }
      }
    });
  }, POLLING_INTERVAL_MS);
};
Enter fullscreen mode Exit fullscreen mode

Here, we check for events of type botResponse and then perform some basic deduplication to ensure messages aren't sent to Discord more than once.

Going back to the main request-response handler, we now need to update event timestamp and question history and start a polling from above for a given user.

lastJaicpMessageTimestamps[message.author.id] = Date.parse(
        response.data.timestamp
      );
lastQuestionIds[message.author.id] = response.data.questionId;
if (!pollingLoops.hasOwnProperty(message.author.id)) {
    pollingLoops[message.author.id] = true;
    startPollingLoopForUser(message.author.id, message.channel);
}
Enter fullscreen mode Exit fullscreen mode

Please note that for the purpose of this article I'm using the very basic data structures and don't persist data between adapter restarts, so this code is by no means production ready, but still give you a decent foundation to build a fully fledged adapter for virtually any chat platform.

When you run a complete example and test it in Discord, it should look something like this ⬇️

Scheduling a random Unsplash picture in Discord via JAICP chatbot

Conclusion

Expanding on Part 1, our Discord chat bot can now can send a random picture at requested time. Moreover, Discord-to-JAICP adapter now can handle both traditional request-response interchange and server initiated events.

As usual, complete source code is available on Github - adapter and chatbot (make sure to check out part-2 branch for both).

Cover photo by Volodymyr Hryshchenko on Unsplash.

💖 💪 🙅 🚩
hiisi13
Dmitry Kozhedubov

Posted on February 4, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related