Unsplash chatbot for Discord, Pt. 2: more ways to bring pictures to Discord
Dmitry Kozhedubov
Posted on February 4, 2022
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;
}
};
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 ⬇️
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}}.
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);
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:
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);
};
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);
}
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 ⬇️
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.
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
February 4, 2022